├── ethereum-contracts ├── .soliumignore ├── .gitignore ├── migrations │ ├── 1_initial_migration.js │ └── 2_next.js ├── .soliumrc.json ├── contracts │ ├── test │ │ ├── TestToken.sol │ │ └── TestProcessor.sol │ ├── Migrations.sol │ ├── Processor.sol │ └── Peggy.sol ├── truffle.js ├── package.json ├── README.md └── test │ ├── test_processor.js │ └── test_peggy.js ├── ethbridge.jpg ├── .gitignore ├── x ├── ethbridge │ ├── common │ │ └── common.go │ ├── types │ │ ├── codec.go │ │ ├── keys.go │ │ ├── errors.go │ │ ├── querytypes.go │ │ ├── msgs.go │ │ ├── test_common.go │ │ └── ethbridgeclaim.go │ ├── ethbridge.go │ ├── client │ │ ├── module_client.go │ │ ├── cli │ │ │ ├── query.go │ │ │ └── tx.go │ │ └── rest │ │ │ └── rest.go │ ├── handler.go │ ├── querier │ │ ├── querier_test.go │ │ └── querier.go │ └── handler_test.go └── oracle │ ├── types │ ├── test_common.go │ ├── keys.go │ ├── errors.go │ └── prophecy.go │ ├── oracle.go │ └── keeper │ ├── keeper.go │ ├── test_common.go │ └── keeper_test.go ├── cmd ├── ebrelayer │ ├── contract │ │ ├── contract_test.go │ │ ├── contract.go │ │ └── PeggyABI.json │ ├── relayer │ │ ├── network_test.go │ │ ├── network.go │ │ ├── relayer_test.go │ │ └── relayer.go │ ├── events │ │ ├── events.go │ │ └── event.go │ ├── txs │ │ ├── parser.go │ │ ├── parser_test.go │ │ └── relay.go │ └── main.go ├── ebcli │ └── main.go └── ebd │ └── main.go ├── Makefile ├── Gopkg.toml ├── .circleci └── config.yml ├── genesis.json ├── genesis.go ├── export.go ├── cosmos-ethereum-bridge.postman_collection.json ├── app.go ├── README.md └── LICENSE /ethereum-contracts/.soliumignore: -------------------------------------------------------------------------------- 1 | **/**node_modules 2 | -------------------------------------------------------------------------------- /ethereum-contracts/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /ethbridge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swishlabsco/cosmos-ethereum-bridge/HEAD/ethbridge.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_Store 3 | *.swp 4 | *.swo 5 | .vscode 6 | .idea 7 | 8 | # Build 9 | vendor 10 | .vendor-new 11 | build 12 | 13 | # IDE 14 | .idea/ 15 | *.iml 16 | -------------------------------------------------------------------------------- /ethereum-contracts/migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | var Migrations = artifacts.require("./Migrations.sol"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; 6 | -------------------------------------------------------------------------------- /x/ethbridge/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import gethCommon "github.com/ethereum/go-ethereum/common" 4 | 5 | //IsValidEthereumAddress returns true if address is valid 6 | func IsValidEthAddress(s string) bool { 7 | return gethCommon.IsHexAddress(s) 8 | } 9 | -------------------------------------------------------------------------------- /ethereum-contracts/.soliumrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solium:recommended", 3 | "plugins": [ 4 | "security" 5 | ], 6 | "rules": { 7 | "quotes": [ 8 | "error", 9 | "double" 10 | ], 11 | "indentation": [ 12 | "error", 13 | 4 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /x/ethbridge/types/codec.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/cosmos/cosmos-sdk/codec" 5 | ) 6 | 7 | // RegisterCodec registers concrete types on the Amino codec 8 | func RegisterCodec(cdc *codec.Codec) { 9 | cdc.RegisterConcrete(MsgMakeEthBridgeClaim{}, "oracle/MsgMakeEthBridgeClaim", nil) 10 | } 11 | -------------------------------------------------------------------------------- /x/oracle/types/test_common.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // sdk "github.com/cosmos/cosmos-sdk/types" 4 | 5 | const ( 6 | TestID = "oracleID" 7 | AlternateTestID = "altOracleID" 8 | TestString = "{value: 5}" 9 | AlternateTestString = "{value: 7}" 10 | AnotherAlternateTestString = "{value: 9}" 11 | ) 12 | -------------------------------------------------------------------------------- /ethereum-contracts/migrations/2_next.js: -------------------------------------------------------------------------------- 1 | var Peggy = artifacts.require("Peggy") 2 | 3 | module.exports = function(deployer, network, accounts) { 4 | // Use deployer to state migration tasks. 5 | var valset 6 | switch (network) { 7 | case "ganache": 8 | default: 9 | valset = [[accounts[0]], [100]] 10 | break 11 | } 12 | deployer.deploy(Peggy) 13 | } 14 | -------------------------------------------------------------------------------- /x/oracle/types/keys.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | const ( 4 | // ModuleName is the name of the oracle module 5 | ModuleName = "oracle" 6 | 7 | // StoreKey is the string store representation 8 | StoreKey = ModuleName 9 | 10 | // QuerierRoute is the querier route for the oracle module 11 | QuerierRoute = ModuleName 12 | 13 | // RouterKey is the msg router key for the oracle module 14 | RouterKey = ModuleName 15 | ) 16 | -------------------------------------------------------------------------------- /ethereum-contracts/contracts/test/TestToken.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol"; 4 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 5 | 6 | contract TestToken is ERC20Mintable { 7 | 8 | using SafeMath for uint256; 9 | 10 | string public constant name = "Test Token"; 11 | string public constant symbol = "TEST"; 12 | uint8 public constant decimals = 18; 13 | 14 | } -------------------------------------------------------------------------------- /x/ethbridge/types/keys.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | const ( 4 | // ModuleName is the name of the ethereum bridge module 5 | ModuleName = "ethbridge" 6 | 7 | // StoreKey is the string store representation 8 | StoreKey = ModuleName 9 | 10 | // QuerierRoute is the querier route for the ethereum bridge module 11 | QuerierRoute = ModuleName 12 | 13 | // RouterKey is the msg router key for the ethereum bridge module 14 | RouterKey = ModuleName 15 | ) 16 | -------------------------------------------------------------------------------- /cmd/ebrelayer/contract/contract_test.go: -------------------------------------------------------------------------------- 1 | package contract 2 | 3 | import ( 4 | "testing" 5 | "io/ioutil" 6 | "strings" 7 | "log" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | // Set up data for parameters and to compare against 13 | func TestLoadABI(t *testing.T) { 14 | 15 | //Get the ABI ready 16 | rawContractAbi, errorMsg := ioutil.ReadFile("./PeggyABI.json") 17 | if errorMsg != nil { 18 | log.Fatal(errorMsg) 19 | } 20 | 21 | require.True(t, strings.Contains(string(rawContractAbi), "LogLock")) 22 | } -------------------------------------------------------------------------------- /cmd/ebrelayer/relayer/network_test.go: -------------------------------------------------------------------------------- 1 | package relayer 2 | 3 | // ------------------------------------------------------------ 4 | // Network_test 5 | // 6 | // Tests network.go functionality. 7 | // 8 | // ------------------------------------------------------------ 9 | 10 | import ( 11 | "testing" 12 | "fmt" 13 | 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | const ( 18 | Client = "wss://ropsten.infura.io/ws" 19 | ) 20 | 21 | func TestIsWebsocketURL(t *testing.T) { 22 | result := IsWebsocketURL(Client) 23 | require.True(t, result) 24 | } 25 | 26 | func TestSetupWebsocketEthClient(t *testing.T) { 27 | client, err := SetupWebsocketEthClient(Client) 28 | 29 | require.NoError(t, err) 30 | fmt.Printf("%+v", client) 31 | } 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DEP := $(shell command -v dep 2> /dev/null) 2 | 3 | ldflags = -X github.com/cosmos/sdk-application-tutorial/version.Version=$(VERSION) \ 4 | -X github.com/cosmos/sdk-application-tutorial/version.Commit=$(COMMIT) 5 | 6 | get_tools: 7 | ifndef DEP 8 | @echo "Installing dep" 9 | go get -u -v github.com/golang/dep/cmd/dep 10 | else 11 | @echo "Dep is already installed..." 12 | endif 13 | 14 | get_vendor_deps: 15 | @echo "--> Generating vendor directory via dep ensure" 16 | @rm -rf .vendor-new 17 | @dep ensure -v -vendor-only 18 | 19 | update_vendor_deps: 20 | @echo "--> Running dep ensure" 21 | @rm -rf .vendor-new 22 | @dep ensure -v -update 23 | 24 | install: 25 | go install ./cmd/ebd 26 | go install ./cmd/ebcli 27 | go install ./cmd/ebrelayer 28 | 29 | test: 30 | gotestsum -------------------------------------------------------------------------------- /x/ethbridge/types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | sdk "github.com/cosmos/cosmos-sdk/types" 5 | ) 6 | 7 | // Local code type 8 | type CodeType = sdk.CodeType 9 | 10 | //Exported code type numbers 11 | const ( 12 | DefaultCodespace sdk.CodespaceType = "ethbridge" 13 | 14 | CodeInvalidEthNonce CodeType = 1 15 | CodeInvalidEthAddress CodeType = 2 16 | ) 17 | 18 | func ErrInvalidEthNonce(codespace sdk.CodespaceType) sdk.Error { 19 | return sdk.NewError(codespace, CodeInvalidEthNonce, "invalid ethereum nonce provided, must be >= 0") 20 | } 21 | 22 | func ErrInvalidEthAddress(codespace sdk.CodespaceType) sdk.Error { 23 | return sdk.NewError(codespace, CodeInvalidEthAddress, "invalid ethereum address provided, must be a valid hex-encoded Ethereum address") 24 | } 25 | -------------------------------------------------------------------------------- /ethereum-contracts/contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | // A function with the signature `last_completed_migration()`, returning a uint, is required. 6 | uint public last_completed_migration; 7 | 8 | modifier restricted() { 9 | if (msg.sender == owner) _; 10 | } 11 | 12 | constructor() public { 13 | owner = msg.sender; 14 | } 15 | 16 | // A function with the signature `setCompleted(uint)` is required. 17 | function setCompleted(uint completed) public restricted { 18 | last_completed_migration = completed; 19 | } 20 | 21 | function upgrade(address new_address) public restricted { 22 | Migrations upgraded = Migrations(new_address); 23 | upgraded.setCompleted(last_completed_migration); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ethereum-contracts/truffle.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | networks: { 3 | development: { 4 | host: "localhost", 5 | port: 8545, 6 | network_id: "*", 7 | gas: 6000000, 8 | gasPrice: 200000000000, 9 | solc: { 10 | version: "0.5.0", 11 | optimizer: { 12 | enabled: true, 13 | runs: 200 14 | } 15 | } 16 | }, 17 | ganache: { 18 | host: "127.0.0.1", 19 | port: 7545, 20 | network_id: "*" 21 | }, 22 | ropsten: { 23 | network_id: 3, 24 | host: "localhost", 25 | port: 8545, 26 | gas: 6000000, 27 | solc: { optimizer: { enabled: true, runs: 200 } } 28 | } 29 | }, 30 | rpc: { 31 | host: 'localhost', 32 | post:8080 33 | }, 34 | mocha: { 35 | useColors: true 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /cmd/ebrelayer/contract/contract.go: -------------------------------------------------------------------------------- 1 | package contract 2 | 3 | // ------------------------------------------------------- 4 | // Contract 5 | // 6 | // Contains functionality related to the smart contract 7 | // ------------------------------------------------------- 8 | 9 | import ( 10 | "io/ioutil" 11 | "log" 12 | "strings" 13 | 14 | "github.com/ethereum/go-ethereum/accounts/abi" 15 | ) 16 | 17 | func LoadABI() abi.ABI { 18 | // Open the file containing Peggy contract's ABI 19 | rawContractAbi, errorMsg := ioutil.ReadFile("cmd/ebrelayer/contract/PeggyABI.json") 20 | if errorMsg != nil { 21 | log.Fatal(errorMsg) 22 | } 23 | 24 | // Convert the raw abi into a usable format 25 | contractAbi, err := abi.JSON(strings.NewReader(string(rawContractAbi))) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | return contractAbi 31 | } 32 | -------------------------------------------------------------------------------- /x/ethbridge/ethbridge.go: -------------------------------------------------------------------------------- 1 | package ethbridge 2 | 3 | import ( 4 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/querier" 5 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/types" 6 | ) 7 | 8 | type ( 9 | MsgMakeEthBridgeClaim = types.MsgMakeEthBridgeClaim 10 | ) 11 | 12 | var ( 13 | NewMsgMakeEthBridgeClaim = types.NewMsgMakeEthBridgeClaim 14 | NewEthBridgeClaim = types.NewEthBridgeClaim 15 | 16 | NewQueryEthProphecyParams = types.NewQueryEthProphecyParams 17 | 18 | ErrInvalidEthNonce = types.ErrInvalidEthNonce 19 | 20 | RegisterCodec = types.RegisterCodec 21 | 22 | NewQuerier = querier.NewQuerier 23 | ) 24 | 25 | const ( 26 | StoreKey = types.StoreKey 27 | QuerierRoute = types.QuerierRoute 28 | RouterKey = types.RouterKey 29 | DefaultCodespace = types.DefaultCodespace 30 | 31 | QueryEthProphecy = querier.QueryEthProphecy 32 | ) 33 | -------------------------------------------------------------------------------- /ethereum-contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethereum-contracts", 3 | "version": "1.0.0", 4 | "description": "Starting point for cross chain value transfers between the Ethereum and Cosmos networks", 5 | "main": "truffle.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "author": "Denali Marsh", 10 | "license": "ISC", 11 | "dependencies": { 12 | "bignumber.js": "^6.0.0", 13 | "bluebird": "^3.5.1", 14 | "eth-lib": "^0.2.8", 15 | "ethereumjs-util": "^5.2.0", 16 | "keccak": "^1.4.0", 17 | "keythereum": "^1.0.4", 18 | "lodash": "^4.17.10", 19 | "utf8": "^3.0.0" 20 | }, 21 | "devDependencies": { 22 | "chai": "^4.2.0", 23 | "chai-as-promised": "^7.1.1", 24 | "chai-bignumber": "^3.0.0", 25 | "openzeppelin-solidity": "^2.1.3", 26 | "solc": "^0.5.0", 27 | "web3": "^1.0.0-beta.52", 28 | "web3-utils": "^1.0.0-beta.52" 29 | }, 30 | "scripts": { 31 | "develop": "truffle develop", 32 | "test": "truffle test", 33 | "exit": ".exit" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /x/oracle/oracle.go: -------------------------------------------------------------------------------- 1 | package oracle 2 | 3 | import ( 4 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle/keeper" 5 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle/types" 6 | ) 7 | 8 | // DefaultConsensusNeeded is the default fraction of validators needed to make claims on a prophecy in order for it to pass 9 | const DefaultConsensusNeeded float64 = 0.7 10 | 11 | type ( 12 | Keeper = keeper.Keeper 13 | 14 | Prophecy = types.Prophecy 15 | 16 | Status = types.Status 17 | ) 18 | 19 | var ( 20 | NewKeeper = keeper.NewKeeper 21 | 22 | NewProphecy = types.NewProphecy 23 | ) 24 | 25 | const ( 26 | PendingStatus = types.PendingStatusText 27 | SuccessStatus = types.SuccessStatusText 28 | FailedStatus = types.FailedStatusText 29 | ) 30 | 31 | const ( 32 | StoreKey = types.StoreKey 33 | QuerierRoute = types.QuerierRoute 34 | RouterKey = types.RouterKey 35 | DefaultCodespace = types.DefaultCodespace 36 | 37 | TestID = types.TestID 38 | ) 39 | 40 | var ( 41 | ErrProphecyNotFound = types.ErrProphecyNotFound 42 | ErrMinimumConsensusNeededInvalid = types.ErrMinimumConsensusNeededInvalid 43 | ErrInvalidIdentifier = types.ErrInvalidIdentifier 44 | ) 45 | -------------------------------------------------------------------------------- /cmd/ebrelayer/relayer/network.go: -------------------------------------------------------------------------------- 1 | package relayer 2 | 3 | // ------------------------------------------------------------ 4 | // Network 5 | // 6 | // Validates input and initializes a websocket Ethereum 7 | // client. 8 | // ------------------------------------------------------------ 9 | 10 | import ( 11 | "fmt" 12 | "net/url" 13 | 14 | "github.com/ethereum/go-ethereum/ethclient" 15 | log "github.com/golang/glog" 16 | ) 17 | 18 | // IsWebsocketURL return true if the given URL is a websocket URL 19 | func IsWebsocketURL(rawurl string) bool { 20 | u, err := url.Parse(rawurl) 21 | if err != nil { 22 | log.Infof("Error while parsing URL: %v", err) 23 | return false 24 | } 25 | if u.Scheme == "ws" || u.Scheme == "wss" { 26 | return true 27 | } 28 | return false 29 | } 30 | 31 | // SetupWebsocketEthClient returns an websocket ethclient if URL is valid. 32 | func SetupWebsocketEthClient(ethURL string) (*ethclient.Client, error) { 33 | if ethURL == "" { 34 | return nil, nil 35 | } 36 | 37 | if !IsWebsocketURL(ethURL) { 38 | return nil, fmt.Errorf( 39 | "In valid websocket eth client URL: %v", 40 | ethURL, 41 | ) 42 | } 43 | 44 | client, err := ethclient.Dial(ethURL) 45 | if err != nil { 46 | return nil, fmt.Errorf("error dialing websocket client") 47 | } 48 | 49 | return client, nil 50 | } 51 | -------------------------------------------------------------------------------- /cmd/ebrelayer/events/events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // ----------------------------------------------------- 4 | // Events 5 | // 6 | // Events maintains a mapping of events to an array 7 | // of claims made by validators. 8 | // ----------------------------------------------------- 9 | 10 | import ( 11 | "fmt" 12 | 13 | ) 14 | 15 | var EventRecords = make(map[string]LockEvent) 16 | 17 | // Add a validator's address to the official claims list 18 | func NewEventWrite(txHash string, event LockEvent) bool { 19 | EventRecords[txHash] = event 20 | 21 | return true 22 | } 23 | 24 | // Checks the sessions stored events for this transaction hash 25 | func IsEventRecorded(txHash string) bool { 26 | if EventRecords[txHash].Nonce == nil { 27 | return false 28 | } 29 | return true 30 | } 31 | 32 | func PrintEventByTx(txHash string) { 33 | if IsEventRecorded(txHash) { 34 | PrintEvent(EventRecords[txHash]) 35 | } else { 36 | fmt.Printf("\nNo records from this sesson for tx: %v\n", txHash) 37 | } 38 | } 39 | 40 | // Prints all the claims made on this event 41 | func PrintEvents() error { 42 | 43 | // For each claim, print the validator which submitted the claim 44 | for tx, event := range EventRecords { 45 | fmt.Printf("\nTransaction: %v\n", tx) 46 | PrintEvent(event) 47 | } 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /ethereum-contracts/contracts/test/TestProcessor.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "../Processor.sol"; 4 | 5 | contract TestProcessor is Processor { 6 | 7 | event LogItemCreated(bytes32 _id); 8 | 9 | function() external payable {} 10 | 11 | //Wrapper function to test internal method 12 | function callCreate( 13 | address payable _sender, 14 | bytes memory _recipient, 15 | address _token, 16 | uint256 _amount 17 | ) 18 | public 19 | returns(bytes32) 20 | { 21 | bytes32 id = create(_sender, _recipient, _token, _amount); 22 | emit LogItemCreated(id); 23 | return id; 24 | } 25 | 26 | //Wrapper function to test internal method 27 | function callComplete( 28 | bytes32 _id 29 | ) 30 | public 31 | { 32 | complete(_id); 33 | } 34 | 35 | //Wrapper function to test internal method 36 | function callIsLocked( 37 | bytes32 _id 38 | ) 39 | public 40 | view 41 | returns(bool) 42 | { 43 | return isLocked(_id); 44 | } 45 | 46 | //Wrapper function to test internal method 47 | function callGetItem( 48 | bytes32 _id 49 | ) 50 | public 51 | view 52 | returns(address, bytes memory, address, uint256, uint256) 53 | { 54 | return getItem(_id); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /x/ethbridge/types/querytypes.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle" 8 | ) 9 | 10 | // defines the params for the following queries: 11 | // - 'custom/ethbridge/prophecies/' 12 | type QueryEthProphecyParams struct { 13 | Nonce int 14 | EthereumSender string 15 | } 16 | 17 | func NewQueryEthProphecyParams(nonce int, ethereumSender string) QueryEthProphecyParams { 18 | return QueryEthProphecyParams{ 19 | Nonce: nonce, 20 | EthereumSender: ethereumSender, 21 | } 22 | } 23 | 24 | // Query Result Payload for an eth prophecy query 25 | type QueryEthProphecyResponse struct { 26 | ID string `json:"id"` 27 | Status oracle.Status `json:"status"` 28 | EthBridgeClaims []EthBridgeClaim `json:"claims"` 29 | } 30 | 31 | func NewQueryEthProphecyResponse(id string, status oracle.Status, claims []EthBridgeClaim) QueryEthProphecyResponse { 32 | return QueryEthProphecyResponse{ 33 | ID: id, 34 | Status: status, 35 | EthBridgeClaims: claims, 36 | } 37 | } 38 | 39 | func (response QueryEthProphecyResponse) String() string { 40 | prophecyJSON, err := json.Marshal(response) 41 | if err != nil { 42 | return fmt.Sprintf("Error marshalling json: %v", err) 43 | } 44 | 45 | return string(prophecyJSON) 46 | } 47 | -------------------------------------------------------------------------------- /x/ethbridge/client/module_client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/cosmos/cosmos-sdk/client" 5 | "github.com/spf13/cobra" 6 | ethbridgecmd "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/client/cli" 7 | amino "github.com/tendermint/go-amino" 8 | ) 9 | 10 | // ModuleClient exports all client functionality from this module 11 | type ModuleClient struct { 12 | queryRoute string 13 | cdc *amino.Codec 14 | } 15 | 16 | func NewModuleClient(queryRoute string, cdc *amino.Codec) ModuleClient { 17 | return ModuleClient{queryRoute, cdc} 18 | } 19 | 20 | // GetQueryCmd returns the cli query commands for this module 21 | func (mc ModuleClient) GetQueryCmd() *cobra.Command { 22 | // Group ethbridge queries under a subcommand 23 | ethBBridgeQueryCmd := &cobra.Command{ 24 | Use: "ethbridge", 25 | Short: "Querying commands for the ethbridge module", 26 | } 27 | 28 | ethBBridgeQueryCmd.AddCommand(client.GetCommands( 29 | ethbridgecmd.GetCmdGetEthBridgeProphecy(mc.queryRoute, mc.cdc), 30 | )...) 31 | 32 | return ethBBridgeQueryCmd 33 | } 34 | 35 | // GetTxCmd returns the transaction commands for this module 36 | func (mc ModuleClient) GetTxCmd() *cobra.Command { 37 | ethBridgeTxCmd := &cobra.Command{ 38 | Use: "ethbridge", 39 | Short: "EthBridge transactions subcommands", 40 | } 41 | 42 | ethBridgeTxCmd.AddCommand(client.PostCommands( 43 | ethbridgecmd.GetCmdMakeEthBridgeClaim(mc.cdc), 44 | )...) 45 | 46 | return ethBridgeTxCmd 47 | } 48 | -------------------------------------------------------------------------------- /cmd/ebrelayer/relayer/relayer_test.go: -------------------------------------------------------------------------------- 1 | package relayer 2 | 3 | // ------------------------------------------------------------ 4 | // Relayer_Test 5 | // 6 | // Tests Relayer functionality. 7 | // 8 | // `go test network.go relayer.go relayer_test.go` 9 | // ------------------------------------------------------------ 10 | 11 | import ( 12 | "testing" 13 | "fmt" 14 | "strings" 15 | "encoding/hex" 16 | 17 | "github.com/ethereum/go-ethereum/common" 18 | "github.com/stretchr/testify/require" 19 | app "github.com/swishlabsco/cosmos-ethereum-bridge" 20 | ) 21 | 22 | const ( 23 | ChainID = "testing" 24 | Socket = "wss://ropsten.infura.io/ws" 25 | ContractAddress = "3de4ef81Ba6243A60B0a32d3BCeD4173b6EA02bb" 26 | EventSig = "0xe154a56f2d306d5bbe4ac2379cb0cfc906b23685047a2bd2f5f0a0e810888f72" 27 | Validator = "validator" 28 | ) 29 | 30 | func TestInitRelayer(t *testing.T) { 31 | cdc := app.MakeCodec() 32 | 33 | // Parse the address of the deployed contract 34 | bytesContractAddress, err := hex.DecodeString(ContractAddress) 35 | if err != nil { 36 | fmt.Printf("Invalid contract-address: %v", bytesContractAddress) 37 | } 38 | contractAddress := common.BytesToAddress(bytesContractAddress) 39 | 40 | err = InitRelayer(cdc, ChainID, Socket, contractAddress, EventSig, Validator) 41 | 42 | //TODO: add validator key processing for relayer init 43 | require.Error(t, err) 44 | require.True(t, strings.Contains(err.Error(), "Key validator not found")) 45 | } 46 | -------------------------------------------------------------------------------- /x/ethbridge/client/cli/query.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/cosmos/cosmos-sdk/client/context" 8 | "github.com/cosmos/cosmos-sdk/codec" 9 | "github.com/spf13/cobra" 10 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge" 11 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/types" 12 | ) 13 | 14 | // GetCmdGetEthBridgeProphecy queries information about a specific prophecy 15 | func GetCmdGetEthBridgeProphecy(queryRoute string, cdc *codec.Codec) *cobra.Command { 16 | return &cobra.Command{ 17 | Use: "get-prophecy nonce ethereum-sender", 18 | Short: "get prophecy", 19 | Args: cobra.ExactArgs(2), 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | cliCtx := context.NewCLIContext().WithCodec(cdc) 22 | nonce := args[0] 23 | 24 | nonceString, err := strconv.Atoi(nonce) 25 | if err != nil { 26 | fmt.Printf(err.Error()) 27 | return nil 28 | } 29 | ethereumSender := args[1] 30 | 31 | bz, err := cdc.MarshalJSON(ethbridge.NewQueryEthProphecyParams(nonceString, ethereumSender)) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | route := fmt.Sprintf("custom/%s/%s", queryRoute, ethbridge.QueryEthProphecy) 37 | res, err := cliCtx.QueryWithData(route, bz) 38 | if err != nil { 39 | fmt.Printf(err.Error()) 40 | return nil 41 | } 42 | 43 | var out types.QueryEthProphecyResponse 44 | cdc.MustUnmarshalJSON(res, &out) 45 | return cliCtx.PrintOutput(out) 46 | }, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cmd/ebrelayer/events/event.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // ----------------------------------------------------- 4 | // Event 5 | // 6 | // Creates LockEvents from new events on the ethereum 7 | // Ethereum blockchain. 8 | // ----------------------------------------------------- 9 | 10 | import ( 11 | "encoding/hex" 12 | "fmt" 13 | "log" 14 | "math/big" 15 | 16 | "github.com/ethereum/go-ethereum/common" 17 | "github.com/ethereum/go-ethereum/accounts/abi" 18 | 19 | ) 20 | 21 | // LockEvent represents a single smart contract event 22 | type LockEvent struct { 23 | Id [32]byte 24 | From common.Address 25 | To []byte 26 | Token common.Address 27 | Value *big.Int 28 | Nonce *big.Int 29 | } 30 | 31 | func NewLockEvent(contractAbi abi.ABI, eventName string, eventData []byte) LockEvent { 32 | 33 | // Load Peggy smart contract abi 34 | if eventName != "LogLock" { 35 | log.Fatal("Only LogLock events are currently supported.") 36 | } 37 | 38 | // Parse the event's attributes as Ethereum network variables 39 | event := LockEvent{} 40 | err := contractAbi.Unpack(&event, eventName, eventData) 41 | if err != nil { 42 | log.Fatal("Unpacking: ", err) 43 | } 44 | 45 | PrintEvent(event) 46 | 47 | return event 48 | } 49 | 50 | func PrintEvent(event LockEvent) { 51 | // Convert the variables into a printable format 52 | id := hex.EncodeToString(event.Id[:]) 53 | sender := event.From.Hex() 54 | recipient := string(event.To[:]) 55 | token := event.Token.Hex() 56 | value := event.Value 57 | nonce := event.Nonce 58 | 59 | // Print the event's information 60 | fmt.Printf("\nEvent ID: %v\nToken: %v\nSender: %v\nRecipient: %v\nValue: %v\nNonce: %v\n\n", 61 | id, token, sender, recipient, value, nonce) 62 | } -------------------------------------------------------------------------------- /x/ethbridge/types/msgs.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/common" 8 | ) 9 | 10 | // MsgMakeEthBridgeClaim defines a message for creating claims on the ethereum bridge 11 | type MsgMakeEthBridgeClaim struct { 12 | EthBridgeClaim `json:"eth_bridge_claim"` 13 | } 14 | 15 | // NewMsgMakeEthBridgeClaim is a constructor function for MsgMakeBridgeClaim 16 | func NewMsgMakeEthBridgeClaim(ethBridgeClaim EthBridgeClaim) MsgMakeEthBridgeClaim { 17 | return MsgMakeEthBridgeClaim{ethBridgeClaim} 18 | } 19 | 20 | // Route should return the name of the module 21 | func (msg MsgMakeEthBridgeClaim) Route() string { return RouterKey } 22 | 23 | // Type should return the action 24 | func (msg MsgMakeEthBridgeClaim) Type() string { return "make_bridge_claim" } 25 | 26 | // ValidateBasic runs stateless checks on the message 27 | func (msg MsgMakeEthBridgeClaim) ValidateBasic() sdk.Error { 28 | if msg.EthBridgeClaim.CosmosReceiver.Empty() { 29 | return sdk.ErrInvalidAddress(msg.CosmosReceiver.String()) 30 | } 31 | if msg.EthBridgeClaim.Nonce < 0 { 32 | return ErrInvalidEthNonce(DefaultCodespace) 33 | } 34 | if !common.IsValidEthAddress(msg.EthBridgeClaim.EthereumSender) { 35 | return ErrInvalidEthAddress(DefaultCodespace) 36 | } 37 | return nil 38 | } 39 | 40 | // GetSignBytes encodes the message for signing 41 | func (msg MsgMakeEthBridgeClaim) GetSignBytes() []byte { 42 | b, err := json.Marshal(msg) 43 | if err != nil { 44 | panic(err) 45 | } 46 | return sdk.MustSortJSON(b) 47 | } 48 | 49 | // GetSigners defines whose signature is required 50 | func (msg MsgMakeEthBridgeClaim) GetSigners() []sdk.AccAddress { 51 | return []sdk.AccAddress{msg.EthBridgeClaim.Validator} 52 | } 53 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | [[constraint]] 28 | name = "github.com/cosmos/cosmos-sdk" 29 | version = "v0.33.0" 30 | 31 | [[override]] 32 | name = "github.com/golang/protobuf" 33 | version = "=1.3.0" 34 | 35 | [[constraint]] 36 | name = "github.com/spf13/cobra" 37 | version = "~0.0.1" 38 | 39 | [[constraint]] 40 | name = "github.com/spf13/viper" 41 | version = "~1.0.0" 42 | 43 | [[override]] 44 | name = "github.com/tendermint/go-amino" 45 | version = "v0.14.1" 46 | 47 | [[override]] 48 | name = "github.com/tendermint/tendermint" 49 | revision = "v0.31.0-dev0" 50 | 51 | [[override]] 52 | name = "github.com/tendermint/iavl" 53 | version = "=v0.12.1" 54 | 55 | [[override]] 56 | name = "golang.org/x/crypto" 57 | source = "https://github.com/swishlabsco/crypto" 58 | revision = "76a94ff009f01d8d397f162298bbb366fefbc539" 59 | 60 | [[override]] 61 | name = "github.com/otiai10/copy" 62 | revision = "7e9a647135a142c2669943d4a4d29be015ce9392" 63 | 64 | [[override]] 65 | name = "github.com/btcsuite/btcd" 66 | revision = "ed77733ec07dfc8a513741138419b8d9d3de9d2d" 67 | 68 | [prune] 69 | go-tests = true 70 | unused-packages = true 71 | -------------------------------------------------------------------------------- /cmd/ebrelayer/txs/parser.go: -------------------------------------------------------------------------------- 1 | package txs 2 | 3 | // -------------------------------------------------------- 4 | // Parser 5 | // 6 | // Parses structs containing event information into 7 | // unsigned transactions for validators to sign, then 8 | // relays the data packets as transactions on the 9 | // Cosmos Bridge. 10 | // -------------------------------------------------------- 11 | 12 | import ( 13 | "strings" 14 | "strconv" 15 | "fmt" 16 | 17 | sdk "github.com/cosmos/cosmos-sdk/types" 18 | "github.com/swishlabsco/cosmos-ethereum-bridge/cmd/ebrelayer/events" 19 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/types" 20 | ) 21 | 22 | func ParsePayload(validator sdk.AccAddress, event *events.LockEvent) (types.EthBridgeClaim, error) { 23 | 24 | witnessClaim := types.EthBridgeClaim{} 25 | 26 | // Nonce type casting (*big.Int -> int) 27 | nonce, nonceErr := strconv.Atoi(event.Nonce.String()) 28 | if nonceErr != nil { 29 | fmt.Errorf("%s", nonceErr) 30 | } 31 | witnessClaim.Nonce = nonce 32 | 33 | // EthereumSender type casting (address.common -> string) 34 | witnessClaim.EthereumSender = event.From.Hex() 35 | 36 | // CosmosReceiver type casting (bytes[] -> sdk.AccAddress) 37 | recipient, recipientErr := sdk.AccAddressFromBech32(string(event.To[:])) 38 | if recipientErr != nil { 39 | fmt.Errorf("%s", recipientErr) 40 | } 41 | witnessClaim.CosmosReceiver = recipient 42 | 43 | // Validator is already the correct type (sdk.AccAddress) 44 | witnessClaim.Validator = validator 45 | 46 | // Amount type casting (*big.Int -> sdk.Coins) 47 | ethereumCoin := []string {event.Value.String(),"ethereum"} 48 | weiAmount, coinErr := sdk.ParseCoins(strings.Join(ethereumCoin, "")) 49 | if coinErr != nil { 50 | fmt.Errorf("%s", coinErr) 51 | } 52 | witnessClaim.Amount = weiAmount 53 | 54 | return witnessClaim, nil 55 | } 56 | -------------------------------------------------------------------------------- /x/ethbridge/types/test_common.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | "github.com/cosmos/cosmos-sdk/codec" 11 | sdk "github.com/cosmos/cosmos-sdk/types" 12 | ) 13 | 14 | const ( 15 | TestAddress = "cosmos1gn8409qq9hnrxde37kuxwx5hrxpfpv8426szuv" 16 | TestValidator = "cosmos1xdp5tvt7lxh8rf9xx07wy2xlagzhq24ha48xtq" 17 | TestNonce = 0 18 | TestEthereumAddress = "0x7B95B6EC7EbD73572298cEf32Bb54FA408207359" 19 | AltTestEthereumAddress = "0x7B95B6EC7EbD73572298cEf32Bb54FA408207344" 20 | TestCoins = "10ethereum" 21 | AltTestCoins = "12ethereum" 22 | ) 23 | 24 | //Ethereum-bridge specific stuff 25 | func CreateTestEthMsg(t *testing.T, validatorAddress sdk.AccAddress) MsgMakeEthBridgeClaim { 26 | ethClaim := CreateTestEthClaim(t, validatorAddress, TestEthereumAddress, TestCoins) 27 | ethMsg := NewMsgMakeEthBridgeClaim(ethClaim) 28 | return ethMsg 29 | } 30 | 31 | func CreateTestEthClaim(t *testing.T, validatorAddress sdk.AccAddress, testEthereumAddress string, coins string) EthBridgeClaim { 32 | testCosmosAddress, err1 := sdk.AccAddressFromBech32(TestAddress) 33 | amount, err2 := sdk.ParseCoins(coins) 34 | require.NoError(t, err1) 35 | require.NoError(t, err2) 36 | ethClaim := NewEthBridgeClaim(TestNonce, testEthereumAddress, testCosmosAddress, validatorAddress, amount) 37 | return ethClaim 38 | } 39 | 40 | func CreateTestQueryEthProphecyResponse(cdc *codec.Codec, t *testing.T, validatorAddress sdk.AccAddress) QueryEthProphecyResponse { 41 | ethBridgeClaim := CreateTestEthClaim(t, validatorAddress, TestEthereumAddress, TestCoins) 42 | id, _, _ := CreateOracleClaimFromEthClaim(cdc, ethBridgeClaim) 43 | ethBridgeClaims := []EthBridgeClaim{ethBridgeClaim} 44 | resp := NewQueryEthProphecyResponse(id, oracle.Status{oracle.PendingStatus, ""}, ethBridgeClaims) 45 | return resp 46 | } 47 | -------------------------------------------------------------------------------- /x/ethbridge/client/cli/tx.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/cosmos/cosmos-sdk/client/context" 7 | "github.com/cosmos/cosmos-sdk/client/utils" 8 | "github.com/cosmos/cosmos-sdk/codec" 9 | "github.com/spf13/cobra" 10 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/types" 11 | 12 | sdk "github.com/cosmos/cosmos-sdk/types" 13 | authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" 14 | ) 15 | 16 | // GetCmdMakeEthBridgeClaim is the CLI command for making a claim on an ethereum prophecy 17 | func GetCmdMakeEthBridgeClaim(cdc *codec.Codec) *cobra.Command { 18 | return &cobra.Command{ 19 | Use: "make-claim nonce ethereum-sender-address cosmos-receiver-address validator-address amount", 20 | Short: "make a claim on an ethereum prophecy", 21 | Args: cobra.ExactArgs(5), 22 | RunE: func(cmd *cobra.Command, args []string) error { 23 | cliCtx := context.NewCLIContext().WithCodec(cdc).WithAccountDecoder(cdc) 24 | 25 | txBldr := authtxb.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) 26 | 27 | if err := cliCtx.EnsureAccountExists(); err != nil { 28 | return err 29 | } 30 | 31 | nonce, stringError := strconv.Atoi(args[0]) 32 | if stringError != nil { 33 | return stringError 34 | } 35 | 36 | ethereumSender := args[1] 37 | cosmosReceiver, err := sdk.AccAddressFromBech32(args[2]) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | validator, err := sdk.AccAddressFromBech32(args[3]) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | amount, err := sdk.ParseCoins(args[4]) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | ethBridgeClaim := types.NewEthBridgeClaim(nonce, ethereumSender, cosmosReceiver, validator, amount) 53 | msg := types.NewMsgMakeEthBridgeClaim(ethBridgeClaim) 54 | err = msg.ValidateBasic() 55 | if err != nil { 56 | return err 57 | } 58 | 59 | cliCtx.PrintResponse = true 60 | 61 | return utils.CompleteAndBroadcastTxCLI(txBldr, cliCtx, []sdk.Msg{msg}) 62 | }, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cmd/ebrelayer/txs/parser_test.go: -------------------------------------------------------------------------------- 1 | package txs 2 | 3 | import ( 4 | "testing" 5 | "fmt" 6 | "math/big" 7 | 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | "github.com/stretchr/testify/require" 10 | "github.com/swishlabsco/cosmos-ethereum-bridge/cmd/ebrelayer/events" 11 | "github.com/ethereum/go-ethereum/common" 12 | ) 13 | 14 | var TestValidator sdk.AccAddress 15 | var TestEventData events.LockEvent 16 | 17 | func init() { 18 | 19 | // Set up testing parameters for the parser 20 | testValidator, err := sdk.AccAddressFromBech32("cosmos1xdp5tvt7lxh8rf9xx07wy2xlagzhq24ha48xtq") 21 | if err != nil { 22 | fmt.Errorf("%s", err) 23 | } 24 | TestValidator = testValidator 25 | 26 | // Mock expected data from the parser 27 | TestEventData := events.LockEvent{} 28 | 29 | var arr [32]byte 30 | copy(arr[:], []byte("0xab85e2ceaa7d100af2f07cac01365f3777153a4e004342dca5db44e731b9d461")) 31 | TestEventData.Id = arr 32 | TestEventData.From = common.BytesToAddress([]byte("0xC8Ee928625908D90d4B60859052aD200CBe2792A")) 33 | TestEventData.To = []byte("0x6e656f") 34 | TestEventData.Token = common.BytesToAddress([]byte("0x0000000000000000000000000000000000000000")) 35 | 36 | value := new(big.Int) 37 | value, okValue := value.SetString("7", 10) 38 | if !okValue { 39 | fmt.Println("SetString: error") 40 | } 41 | TestEventData.Value = value 42 | 43 | nonce := new(big.Int) 44 | nonce, okNonce := nonce.SetString("39", 10) 45 | if !okNonce { 46 | fmt.Println("SetString: error") 47 | } 48 | TestEventData.Nonce = nonce 49 | 50 | fmt.Printf("%+v", TestEventData) 51 | 52 | } 53 | 54 | // Set up data for parameters and to compare against 55 | func TestParsePayload(t *testing.T) { 56 | result, err := ParsePayload(TestValidator, &TestEventData) 57 | 58 | require.NoError(t, err) 59 | fmt.Printf("%+v", result) 60 | 61 | // TODO: check each individual argument 62 | // require.Equal(t, "7", string(result.Nonce)) 63 | // require.Equal(t, common.BytesToAddress([]byte("0xC8Ee928625908D90d4B60859052aD200CBe2792A")), result.EthereumSender) 64 | // require.Equal(t, result.CosmosReceiver, "neo") 65 | // require.Equal(t, result.Validator, TestValidator) 66 | // require.Equal(t, result.Amount, 7) 67 | 68 | } -------------------------------------------------------------------------------- /cmd/ebrelayer/txs/relay.go: -------------------------------------------------------------------------------- 1 | package txs 2 | 3 | // ------------------------------------------------------------ 4 | // Relay 5 | // 6 | // Builds and encodes EthBridgeClaim Msgs with the 7 | // specified variables, before presenting the unsigned 8 | // transaction to validators for optional signing. 9 | // Once signed, the data packets are sent as transactions 10 | // on the Cosmos Bridge. 11 | // ------------------------------------------------------------ 12 | 13 | import ( 14 | "fmt" 15 | 16 | sdk "github.com/cosmos/cosmos-sdk/types" 17 | authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" 18 | amino "github.com/tendermint/go-amino" 19 | 20 | "github.com/cosmos/cosmos-sdk/client/context" 21 | "github.com/cosmos/cosmos-sdk/client/utils" 22 | 23 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge" 24 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/types" 25 | ) 26 | 27 | func RelayEvent(chainId string, cdc *amino.Codec, validatorAddress sdk.AccAddress, validatorName string, passphrase string, claim *types.EthBridgeClaim) error { 28 | 29 | cliCtx := context.NewCLIContext(). 30 | WithCodec(cdc). 31 | WithAccountDecoder(cdc) 32 | 33 | cliCtx = cliCtx. 34 | WithFromAddress(validatorAddress). 35 | WithFromName(validatorName) 36 | 37 | cliCtx.SkipConfirm = true 38 | 39 | txBldr := authtxb.NewTxBuilderFromCLI(). 40 | WithTxEncoder(utils.GetTxEncoder(cdc)). 41 | WithChainID(chainId) 42 | 43 | err := cliCtx.EnsureAccountExistsFromAddr(claim.Validator) 44 | if err != nil { 45 | fmt.Printf("Validator account error: %s", err) 46 | } 47 | 48 | msg := ethbridge.NewMsgMakeEthBridgeClaim(*claim) 49 | 50 | err1 := msg.ValidateBasic() 51 | if err1 != nil { 52 | fmt.Printf("Msg validation error: %s", err1) 53 | } 54 | 55 | cliCtx.PrintResponse = true 56 | 57 | //prepare tx 58 | txBldr, err = utils.PrepareTxBuilder(txBldr, cliCtx) 59 | if err != nil { 60 | fmt.Printf("Msg prepare error: %s", err) 61 | return err 62 | } 63 | 64 | // build and sign the transaction 65 | txBytes, err := txBldr.BuildAndSign(validatorName, passphrase, []sdk.Msg{msg}) 66 | if err != nil { 67 | fmt.Printf("Msg build/sign error: %s", err) 68 | return err 69 | } 70 | 71 | // broadcast to a Tendermint node 72 | res, err := cliCtx.BroadcastTx(txBytes) 73 | if err != nil { 74 | fmt.Printf("Msg broadcast error: %s", err) 75 | return err 76 | } 77 | cliCtx.PrintOutput(res) 78 | return err 79 | } 80 | -------------------------------------------------------------------------------- /x/ethbridge/handler.go: -------------------------------------------------------------------------------- 1 | package ethbridge 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cosmos/cosmos-sdk/codec" 7 | sdk "github.com/cosmos/cosmos-sdk/types" 8 | "github.com/cosmos/cosmos-sdk/x/bank" 9 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/common" 10 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/types" 11 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle" 12 | ) 13 | 14 | // NewHandler returns a handler for "ethbridge" type messages. 15 | func NewHandler(oracleKeeper oracle.Keeper, bankKeeper bank.Keeper, cdc *codec.Codec, codespace sdk.CodespaceType) sdk.Handler { 16 | return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { 17 | switch msg := msg.(type) { 18 | case MsgMakeEthBridgeClaim: 19 | return handleMsgMakeEthBridgeClaim(ctx, cdc, oracleKeeper, bankKeeper, msg, codespace) 20 | default: 21 | errMsg := fmt.Sprintf("Unrecognized ethbridge message type: %v", msg.Type()) 22 | return sdk.ErrUnknownRequest(errMsg).Result() 23 | } 24 | } 25 | } 26 | 27 | // Handle a message to make a bridge claim 28 | func handleMsgMakeEthBridgeClaim(ctx sdk.Context, cdc *codec.Codec, oracleKeeper oracle.Keeper, bankKeeper bank.Keeper, msg MsgMakeEthBridgeClaim, codespace sdk.CodespaceType) sdk.Result { 29 | if msg.CosmosReceiver.Empty() { 30 | return sdk.ErrInvalidAddress(msg.CosmosReceiver.String()).Result() 31 | } 32 | if msg.Nonce < 0 { 33 | return types.ErrInvalidEthNonce(codespace).Result() 34 | } 35 | if !common.IsValidEthAddress(msg.EthereumSender) { 36 | return types.ErrInvalidEthAddress(codespace).Result() 37 | } 38 | oracleId, validator, claimString := types.CreateOracleClaimFromEthClaim(cdc, msg.EthBridgeClaim) 39 | status, err := oracleKeeper.ProcessClaim(ctx, oracleId, validator, claimString) 40 | if err != nil { 41 | return err.Result() 42 | } 43 | if status.StatusText == oracle.SuccessStatus { 44 | err = processSuccessfulClaim(ctx, bankKeeper, status.FinalClaim) 45 | if err != nil { 46 | return err.Result() 47 | } 48 | } 49 | return sdk.Result{Log: status.StatusText} 50 | } 51 | 52 | func processSuccessfulClaim(ctx sdk.Context, bankKeeper bank.Keeper, claim string) sdk.Error { 53 | oracleClaim, err := types.CreateOracleClaimFromOracleString(claim) 54 | if err != nil { 55 | return err 56 | } 57 | receiverAddress := oracleClaim.CosmosReceiver 58 | _, _, err = bankKeeper.AddCoins(ctx, receiverAddress, oracleClaim.Amount) 59 | if err != nil { 60 | return err 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /x/oracle/types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | 6 | sdk "github.com/cosmos/cosmos-sdk/types" 7 | ) 8 | 9 | // Local code type 10 | type CodeType = sdk.CodeType 11 | 12 | //Exported code type numbers 13 | const ( 14 | DefaultCodespace sdk.CodespaceType = "oracle" 15 | 16 | CodeProphecyNotFound CodeType = 1 17 | CodeMinimumConsensusNeededInvalid CodeType = 2 18 | CodeNoClaims CodeType = 3 19 | CodeInvalidIdentifier CodeType = 4 20 | CodeProphecyFinalized CodeType = 5 21 | CodeDuplicateMessage CodeType = 6 22 | CodeInvalidClaim CodeType = 7 23 | CodeInvalidValidator CodeType = 8 24 | CodeInternalDB CodeType = 9 25 | ) 26 | 27 | func ErrProphecyNotFound(codespace sdk.CodespaceType) sdk.Error { 28 | return sdk.NewError(codespace, CodeProphecyNotFound, "prophecy with given id not found") 29 | } 30 | 31 | func ErrMinimumConsensusNeededInvalid(codespace sdk.CodespaceType) sdk.Error { 32 | return sdk.NewError(codespace, CodeMinimumConsensusNeededInvalid, "minimum consensus proportion of validator staking power must be > 0 and <= 1") 33 | } 34 | 35 | func ErrNoClaims(codespace sdk.CodespaceType) sdk.Error { 36 | return sdk.NewError(codespace, CodeNoClaims, "cannot create prophecy without initial claim") 37 | } 38 | 39 | func ErrInvalidIdentifier(codespace sdk.CodespaceType) sdk.Error { 40 | return sdk.NewError(codespace, CodeInvalidIdentifier, "invalid identifier provided, must be a nonempty string") 41 | } 42 | 43 | func ErrProphecyFinalized(codespace sdk.CodespaceType) sdk.Error { 44 | return sdk.NewError(codespace, CodeProphecyFinalized, "Prophecy already finalized") 45 | } 46 | 47 | func ErrDuplicateMessage(codespace sdk.CodespaceType) sdk.Error { 48 | return sdk.NewError(codespace, CodeDuplicateMessage, "Already processed message from validator for this id") 49 | } 50 | 51 | func ErrInvalidClaim(codespace sdk.CodespaceType) sdk.Error { 52 | return sdk.NewError(codespace, CodeInvalidClaim, "Claim cannot be empty string") 53 | } 54 | 55 | func ErrInvalidValidator(codespace sdk.CodespaceType) sdk.Error { 56 | return sdk.NewError(codespace, CodeInvalidValidator, "Claim must be made by actively bonded validator") 57 | } 58 | 59 | func ErrInternalDB(codespace sdk.CodespaceType, err error) sdk.Error { 60 | return sdk.NewError(codespace, CodeInternalDB, fmt.Sprintf("Internal error serializing/deserializing prophecy: %s", err.Error())) 61 | } 62 | -------------------------------------------------------------------------------- /x/ethbridge/querier/querier_test.go: -------------------------------------------------------------------------------- 1 | package querier 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | abci "github.com/tendermint/tendermint/abci/types" 9 | 10 | "github.com/cosmos/cosmos-sdk/codec" 11 | sdk "github.com/cosmos/cosmos-sdk/types" 12 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/types" 13 | keeperLib "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle/keeper" 14 | ) 15 | 16 | var ( 17 | prophecyID0 = "0" 18 | ) 19 | 20 | func TestNewQuerier(t *testing.T) { 21 | cdc := codec.New() 22 | ctx, _, keeper, _, _, _ := keeperLib.CreateTestKeepers(t, 0.7, []int64{3, 3}) 23 | 24 | query := abci.RequestQuery{ 25 | Path: "", 26 | Data: []byte{}, 27 | } 28 | 29 | querier := NewQuerier(keeper, cdc, types.DefaultCodespace) 30 | 31 | //Test wrong paths 32 | bz, err := querier(ctx, []string{"other"}, query) 33 | require.NotNil(t, err) 34 | require.Nil(t, bz) 35 | } 36 | 37 | func TestQueryEthProphecy(t *testing.T) { 38 | cdc := codec.New() 39 | ctx, _, keeper, _, validatorAddresses, _ := keeperLib.CreateTestKeepers(t, 0.7, []int64{3, 7}) 40 | accAddress := sdk.AccAddress(validatorAddresses[0]) 41 | initialEthBridgeClaim := types.CreateTestEthClaim(t, accAddress, types.TestEthereumAddress, types.TestCoins) 42 | oracleId, validator, claimText := types.CreateOracleClaimFromEthClaim(cdc, initialEthBridgeClaim) 43 | _, err := keeper.ProcessClaim(ctx, oracleId, validator, claimText) 44 | require.Nil(t, err) 45 | 46 | testResponse := types.CreateTestQueryEthProphecyResponse(cdc, t, accAddress) 47 | 48 | bz, err2 := cdc.MarshalJSON(types.NewQueryEthProphecyParams(types.TestNonce, types.TestEthereumAddress)) 49 | require.Nil(t, err2) 50 | 51 | query := abci.RequestQuery{ 52 | Path: "/custom/ethbridge/prophecies", 53 | Data: bz, 54 | } 55 | 56 | //Test query 57 | res, err3 := queryEthProphecy(ctx, cdc, query, keeper, types.DefaultCodespace) 58 | require.Nil(t, err3) 59 | 60 | var ethProphecyResp types.QueryEthProphecyResponse 61 | err4 := cdc.UnmarshalJSON(res, ðProphecyResp) 62 | require.Nil(t, err4) 63 | require.True(t, reflect.DeepEqual(ethProphecyResp, testResponse)) 64 | 65 | // Test error with bad request 66 | query.Data = bz[:len(bz)-1] 67 | 68 | _, err5 := queryEthProphecy(ctx, cdc, query, keeper, types.DefaultCodespace) 69 | require.NotNil(t, err5) 70 | 71 | // Test error with nonexistent request 72 | query.Data = bz[:len(bz)-1] 73 | bz2, err6 := cdc.MarshalJSON(types.NewQueryEthProphecyParams(12, "badEthereumAddress")) 74 | require.Nil(t, err6) 75 | 76 | query2 := abci.RequestQuery{ 77 | Path: "/custom/oracle/prophecies", 78 | Data: bz2, 79 | } 80 | 81 | _, err7 := queryEthProphecy(ctx, cdc, query2, keeper, types.DefaultCodespace) 82 | require.NotNil(t, err7) 83 | } 84 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 # use CircleCI 2.0 2 | jobs: # basic units of work in a run 3 | build: # runs not using Workflows must have a `build` job as entry point 4 | docker: # run the steps with Docker 5 | # CircleCI Go images available at: https://hub.docker.com/r/circleci/golang/ 6 | - image: circleci/golang:1.12 # 7 | # directory where steps are run. Path must conform to the Go Workspace requirements 8 | working_directory: /go/src/github.com/swishlabsco/cosmos-ethereum-bridge 9 | 10 | environment: # environment variables for the build itself 11 | TEST_RESULTS: /tmp/test-results # path to where test results will be saved 12 | 13 | steps: # steps that comprise the `build` job 14 | - checkout # check out source code to working directory 15 | - run: mkdir -p $TEST_RESULTS # create the test results directory 16 | 17 | - restore_cache: # restores saved cache if no changes are detected since last run 18 | # Read about caching dependencies: https://circleci.com/docs/2.0/caching/ 19 | keys: 20 | - v1-pkg-cache 21 | 22 | - run: make get_tools 23 | - run: dep ensure -v 24 | - run: go get -u github.com/kardianos/govendor 25 | - run: govendor fetch -tree github.com/ethereum/go-ethereum/crypto/secp256k1 26 | - run: make install 27 | - run: 28 | name: Run tests 29 | # Store the results of our tests in the $TEST_RESULTS directory 30 | command: | 31 | gotestsum --junitfile ${TEST_RESULTS}/unit-tests.xml 32 | - store_test_results: 33 | path: /tmp/test-results 34 | contracts: 35 | # The primary container is an instance of the first image listed. The job's commands run in this container. 36 | docker: 37 | # specify the version you desire here 38 | - image: circleci/node:9.11.1 39 | 40 | working_directory: /go/src/github.com/swishlabsco/cosmos-ethereum-bridge/ethereum-contracts 41 | 42 | environment: # environment variables for the build itself 43 | TEST_RESULTS: /tmp/contract-test-results # path to where test results will be saved 44 | 45 | steps: 46 | - checkout 47 | 48 | # Download and cache dependencies 49 | - restore_cache: 50 | keys: 51 | - v1-dependencies-{{ checksum "package.json" }} 52 | # fallback to using the latest cache if no exact match is found 53 | - v1-dependencies- 54 | 55 | - run: npm install 56 | 57 | - save_cache: 58 | paths: 59 | - node_modules 60 | key: v1-dependencies-{{ checksum "package.json" }} 61 | 62 | - run: truffle develop # start server 63 | - run: test # triggers truffle test 64 | - store_test_results: 65 | path: /tmp/contract-test-results 66 | - run: exit 67 | 68 | -------------------------------------------------------------------------------- /x/ethbridge/types/ethbridgeclaim.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/cosmos/cosmos-sdk/codec" 9 | sdk "github.com/cosmos/cosmos-sdk/types" 10 | ) 11 | 12 | type EthBridgeClaim struct { 13 | Nonce int `json:"nonce"` 14 | EthereumSender string `json:"ethereum_sender"` 15 | CosmosReceiver sdk.AccAddress `json:"cosmos_receiver"` 16 | Validator sdk.AccAddress `json:"validator"` 17 | Amount sdk.Coins `json:"amount"` 18 | } 19 | 20 | // NewEthBridgeClaim is a constructor function for NewEthBridgeClaim 21 | func NewEthBridgeClaim(nonce int, ethereumSender string, cosmosReceiver sdk.AccAddress, validator sdk.AccAddress, amount sdk.Coins) EthBridgeClaim { 22 | return EthBridgeClaim{ 23 | Nonce: nonce, 24 | EthereumSender: ethereumSender, 25 | CosmosReceiver: cosmosReceiver, 26 | Validator: validator, 27 | Amount: amount, 28 | } 29 | } 30 | 31 | //OracleClaim is the details of how the claim for each validator will be stored in the oracle 32 | type OracleClaim struct { 33 | CosmosReceiver sdk.AccAddress `json:"cosmos_receiver"` 34 | Amount sdk.Coins `json:"amount"` 35 | } 36 | 37 | // NewOracleClaim is a constructor function for OracleClaim 38 | func NewOracleClaim(cosmosReceiver sdk.AccAddress, amount sdk.Coins) OracleClaim { 39 | return OracleClaim{ 40 | CosmosReceiver: cosmosReceiver, 41 | Amount: amount, 42 | } 43 | } 44 | 45 | func CreateOracleClaimFromEthClaim(cdc *codec.Codec, ethClaim EthBridgeClaim) (string, sdk.ValAddress, string) { 46 | oracleId := strconv.Itoa(ethClaim.Nonce) + ethClaim.EthereumSender 47 | claimContent := NewOracleClaim(ethClaim.CosmosReceiver, ethClaim.Amount) 48 | claimBytes, _ := json.Marshal(claimContent) 49 | claim := string(claimBytes) 50 | validator := sdk.ValAddress(ethClaim.Validator) 51 | return oracleId, validator, claim 52 | } 53 | 54 | func CreateEthClaimFromOracleString(nonce int, ethereumSender string, validator sdk.ValAddress, oracleClaimString string) (EthBridgeClaim, sdk.Error) { 55 | oracleClaim, err := CreateOracleClaimFromOracleString(oracleClaimString) 56 | if err != nil { 57 | return EthBridgeClaim{}, err 58 | } 59 | 60 | valAccAddress := sdk.AccAddress(validator) 61 | return NewEthBridgeClaim( 62 | nonce, 63 | ethereumSender, 64 | oracleClaim.CosmosReceiver, 65 | valAccAddress, 66 | oracleClaim.Amount, 67 | ), nil 68 | } 69 | 70 | func CreateOracleClaimFromOracleString(oracleClaimString string) (OracleClaim, sdk.Error) { 71 | var oracleClaim OracleClaim 72 | 73 | stringBytes := []byte(oracleClaimString) 74 | errRes := json.Unmarshal(stringBytes, &oracleClaim) 75 | if errRes != nil { 76 | return OracleClaim{}, sdk.ErrInternal(fmt.Sprintf("failed to parse claim: %s", errRes)) 77 | } 78 | 79 | return oracleClaim, nil 80 | } 81 | -------------------------------------------------------------------------------- /x/ethbridge/querier/querier.go: -------------------------------------------------------------------------------- 1 | package querier 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/cosmos/cosmos-sdk/codec" 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/types" 10 | keep "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle/keeper" 11 | oracletypes "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle/types" 12 | abci "github.com/tendermint/tendermint/abci/types" 13 | ) 14 | 15 | //query endpoints supported by the oracle Querier 16 | const ( 17 | QueryEthProphecy = "prophecies" 18 | ) 19 | 20 | // NewQuerier is the module level router for state queries 21 | func NewQuerier(keeper keep.Keeper, cdc *codec.Codec, codespace sdk.CodespaceType) sdk.Querier { 22 | return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) { 23 | switch path[0] { 24 | case QueryEthProphecy: 25 | return queryEthProphecy(ctx, cdc, req, keeper, codespace) 26 | default: 27 | return nil, sdk.ErrUnknownRequest("unknown ethbridge query endpoint") 28 | } 29 | } 30 | } 31 | 32 | func queryEthProphecy(ctx sdk.Context, cdc *codec.Codec, req abci.RequestQuery, keeper keep.Keeper, codespace sdk.CodespaceType) (res []byte, err sdk.Error) { 33 | var params types.QueryEthProphecyParams 34 | 35 | errRes := cdc.UnmarshalJSON(req.Data, ¶ms) 36 | if errRes != nil { 37 | return []byte{}, sdk.ErrInternal(fmt.Sprintf("failed to parse params: %s", err)) 38 | } 39 | 40 | id := strconv.Itoa(params.Nonce) + params.EthereumSender 41 | prophecy, err := keeper.GetProphecy(ctx, id) 42 | if err != nil { 43 | return []byte{}, oracletypes.ErrProphecyNotFound(codespace) 44 | } 45 | 46 | bridgeClaims, err2 := MapOracleClaimsToEthBridgeClaims(params.Nonce, params.EthereumSender, prophecy.ValidatorClaims, types.CreateEthClaimFromOracleString) 47 | if err2 != nil { 48 | return []byte{}, err2 49 | } 50 | 51 | response := types.NewQueryEthProphecyResponse(prophecy.ID, prophecy.Status, bridgeClaims) 52 | 53 | bz, err3 := codec.MarshalJSONIndent(cdc, response) 54 | if err3 != nil { 55 | panic("could not marshal result to JSON") 56 | } 57 | 58 | return bz, nil 59 | } 60 | 61 | func MapOracleClaimsToEthBridgeClaims(nonce int, ethereumSender string, oracleValidatorClaims map[string]string, f func(int, string, sdk.ValAddress, string) (types.EthBridgeClaim, sdk.Error)) ([]types.EthBridgeClaim, sdk.Error) { 62 | mappedClaims := make([]types.EthBridgeClaim, len(oracleValidatorClaims)) 63 | i := 0 64 | for validatorBech32, validatorClaim := range oracleValidatorClaims { 65 | validatorAddress, parseErr := sdk.ValAddressFromBech32(validatorBech32) 66 | if parseErr != nil { 67 | return nil, sdk.ErrInternal(fmt.Sprintf("failed to parse claim: %s", parseErr)) 68 | } 69 | mappedClaim, err := f(nonce, ethereumSender, validatorAddress, validatorClaim) 70 | if err != nil { 71 | return nil, err 72 | } 73 | mappedClaims[i] = mappedClaim 74 | i++ 75 | } 76 | return mappedClaims, nil 77 | } 78 | -------------------------------------------------------------------------------- /genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "genesis_time": "2019-04-25T00:00:00.000000Z", 3 | "chain_id": "testing", 4 | "consensus_params": { 5 | "block_size": { 6 | "max_bytes": "1000000", 7 | "max_gas": "300000" 8 | }, 9 | "evidence": { 10 | "max_age": "100000" 11 | }, 12 | "validator": { 13 | "pub_key_types": [ 14 | "ed25519" 15 | ] 16 | } 17 | }, 18 | "app_hash": "", 19 | "app_state": { 20 | "accounts": [ 21 | { 22 | "address": "cosmos1yxyhu0cj8wz0q2y8j2kfy45g7nxqk4duyd5jty", 23 | "coins": [ 24 | { 25 | "denom": "stake", 26 | "amount": "10000" 27 | } 28 | ], 29 | "sequence_number": "0", 30 | "account_number": "0", 31 | "original_vesting": null, 32 | "delegated_free": null, 33 | "delegated_vesting": null, 34 | "start_time": "0", 35 | "end_time": "0" 36 | } 37 | ], 38 | "auth": { 39 | "collected_fees": null, 40 | "params": { 41 | "MemoCostPerByte": "3", 42 | "MaxMemoCharacters": "256", 43 | "TxSigLimit": "7", 44 | "SigVerifyCostED25519": "590", 45 | "SigVerifyCostSecp256k1": "1000" 46 | } 47 | }, 48 | "staking": { 49 | "pool": { 50 | "not_bonded_tokens": "10000000", 51 | "bonded_tokens": "0" 52 | }, 53 | "params": { 54 | "unbonding_time": "259200000000000", 55 | "max_validators": 300, 56 | "bond_denom": "stake" 57 | }, 58 | "last_total_power": "0", 59 | "last_validator_powers": null, 60 | "validators": null, 61 | "bonds": null, 62 | "unbonding_delegations": null, 63 | "redelegations": null, 64 | "exported": false 65 | }, 66 | "gentxs": [ 67 | { 68 | "type": "auth/StdTx", 69 | "value": { 70 | "msg": [ 71 | { 72 | "type": "cosmos-sdk/MsgCreateValidator", 73 | "value": { 74 | "description": { 75 | "moniker": "TestValidator", 76 | "identity": "", 77 | "website": "", 78 | "details": "" 79 | }, 80 | "commission": { 81 | "rate": "1.000000000000000000", 82 | "max_rate": "1.000000000000000000", 83 | "max_change_rate": "1.000000000000000000" 84 | }, 85 | "delegator_address": "cosmos1yxyhu0cj8wz0q2y8j2kfy45g7nxqk4duyd5jty", 86 | "validator_address": "cosmosvaloper185n9jln2g4w79w39m0g85x2tns6uekqg8hrx3j", 87 | "pubkey": "cosmosvalconspub1zcjduepqxgrjydsw7gc5d2xmz467f9teely02nhj4sd244f3rldmf2sakplsazd4jy", 88 | "value": { 89 | "denom": "stake", 90 | "amount": "10000" 91 | } 92 | } 93 | } 94 | ], 95 | "fee": { 96 | "amount": null, 97 | "gas": "200000" 98 | }, 99 | "signatures": [ 100 | { 101 | "pub_key": { 102 | "type": "tendermint/PubKeySecp256k1", 103 | "value": "AgwqSG/R/k+kUtxO1tVqAMwGvuTDGslMGHdyr8gV7GY7" 104 | }, 105 | "signature": "UGKJKl28PQxxolwJUhskbh/zpXmlRGXk/UgVcW48yYtRR4hdO3PuoyXGSRC8hg1hN6ow+gTMljYLHtzU0EjzrQ==" 106 | } 107 | ], 108 | "memo": "9d56f406e7713fb9cea9ffbfd13b0ec5c6177d37@142.93.26.98:26656" 109 | } 110 | }, 111 | ] -------------------------------------------------------------------------------- /genesis.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | sdk "github.com/cosmos/cosmos-sdk/types" 8 | "github.com/cosmos/cosmos-sdk/x/auth" 9 | "github.com/cosmos/cosmos-sdk/x/bank" 10 | "github.com/cosmos/cosmos-sdk/x/staking" 11 | ) 12 | 13 | type GenesisAccount struct { 14 | Address sdk.AccAddress `json:"address"` 15 | Coins sdk.Coins `json:"coins"` 16 | Sequence uint64 `json:"sequence_number"` 17 | AccountNumber uint64 `json:"account_number"` 18 | 19 | // vesting account fields 20 | OriginalVesting sdk.Coins `json:"original_vesting"` // total vesting coins upon initialization 21 | DelegatedFree sdk.Coins `json:"delegated_free"` // delegated vested coins at time of delegation 22 | DelegatedVesting sdk.Coins `json:"delegated_vesting"` // delegated vesting coins at time of delegation 23 | StartTime int64 `json:"start_time"` // vesting start time (UNIX Epoch time) 24 | EndTime int64 `json:"end_time"` // vesting end time (UNIX Epoch time) 25 | } 26 | 27 | // GenesisState represents chain state at the start of the chain. Any initial state (account balances) are stored here. 28 | type GenesisState struct { 29 | Accounts []GenesisAccount `json:"accounts"` 30 | AuthData auth.GenesisState `json:"auth"` 31 | BankData bank.GenesisState `json:"bank"` 32 | StakingData staking.GenesisState `json:"staking"` 33 | GenTxs []json.RawMessage `json:"gentxs"` 34 | } 35 | 36 | // convert GenesisAccount to auth.BaseAccount 37 | func (ga *GenesisAccount) ToAccount() auth.Account { 38 | bacc := &auth.BaseAccount{ 39 | Address: ga.Address, 40 | Coins: ga.Coins.Sort(), 41 | AccountNumber: ga.AccountNumber, 42 | Sequence: ga.Sequence, 43 | } 44 | 45 | if !ga.OriginalVesting.IsZero() { 46 | baseVestingAcc := &auth.BaseVestingAccount{ 47 | BaseAccount: bacc, 48 | OriginalVesting: ga.OriginalVesting, 49 | DelegatedFree: ga.DelegatedFree, 50 | DelegatedVesting: ga.DelegatedVesting, 51 | EndTime: ga.EndTime, 52 | } 53 | 54 | if ga.StartTime != 0 && ga.EndTime != 0 { 55 | return &auth.ContinuousVestingAccount{ 56 | BaseVestingAccount: baseVestingAcc, 57 | StartTime: ga.StartTime, 58 | } 59 | } else if ga.EndTime != 0 { 60 | return &auth.DelayedVestingAccount{ 61 | BaseVestingAccount: baseVestingAcc, 62 | } 63 | } else { 64 | panic(fmt.Sprintf("invalid genesis vesting account: %+v", ga)) 65 | } 66 | } 67 | 68 | return bacc 69 | } 70 | 71 | func NewGenesisState(accounts []GenesisAccount, authData auth.GenesisState, 72 | bankData bank.GenesisState, 73 | stakingData staking.GenesisState) GenesisState { 74 | 75 | return GenesisState{ 76 | Accounts: accounts, 77 | AuthData: authData, 78 | BankData: bankData, 79 | StakingData: stakingData, 80 | } 81 | } 82 | 83 | func NewGenesisAccount(acc *auth.BaseAccount) GenesisAccount { 84 | return GenesisAccount{ 85 | Address: acc.Address, 86 | Coins: acc.Coins, 87 | AccountNumber: acc.AccountNumber, 88 | Sequence: acc.Sequence, 89 | } 90 | } 91 | 92 | func NewGenesisAccountI(acc auth.Account) GenesisAccount { 93 | gacc := GenesisAccount{ 94 | Address: acc.GetAddress(), 95 | Coins: acc.GetCoins(), 96 | AccountNumber: acc.GetAccountNumber(), 97 | Sequence: acc.GetSequence(), 98 | } 99 | 100 | vacc, ok := acc.(auth.VestingAccount) 101 | if ok { 102 | gacc.OriginalVesting = vacc.GetOriginalVesting() 103 | gacc.DelegatedFree = vacc.GetDelegatedFree() 104 | gacc.DelegatedVesting = vacc.GetDelegatedVesting() 105 | gacc.StartTime = vacc.GetStartTime() 106 | gacc.EndTime = vacc.GetEndTime() 107 | } 108 | 109 | return gacc 110 | } 111 | -------------------------------------------------------------------------------- /cmd/ebrelayer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // ------------------------------------------------------------- 4 | // Main (ebrelayer) 5 | // 6 | // Implements CLI commands for the Relayer service, such as 7 | // initalization and event relay. 8 | // ------------------------------------------------------------- 9 | 10 | import ( 11 | "encoding/hex" 12 | "fmt" 13 | "os" 14 | 15 | "github.com/spf13/cobra" 16 | 17 | "github.com/cosmos/cosmos-sdk/client/rpc" 18 | sdk "github.com/cosmos/cosmos-sdk/types" 19 | "github.com/ethereum/go-ethereum/common" 20 | amino "github.com/tendermint/go-amino" 21 | "github.com/tendermint/tendermint/libs/cli" 22 | 23 | // "golang.org/x/crypto" 24 | 25 | app "github.com/swishlabsco/cosmos-ethereum-bridge" 26 | relayer "github.com/swishlabsco/cosmos-ethereum-bridge/cmd/ebrelayer/relayer" 27 | ) 28 | 29 | const ( 30 | storeAcc = "acc" 31 | routeEthbridge = "ethbridge" 32 | ) 33 | 34 | var defaultCLIHome = os.ExpandEnv("$HOME/.ebcli") 35 | var appCodec *amino.Codec 36 | 37 | func init() { 38 | 39 | // Read in the configuration file for the sdk 40 | config := sdk.GetConfig() 41 | config.SetBech32PrefixForAccount(sdk.Bech32PrefixAccAddr, sdk.Bech32PrefixAccPub) 42 | config.SetBech32PrefixForValidator(sdk.Bech32PrefixValAddr, sdk.Bech32PrefixValPub) 43 | config.SetBech32PrefixForConsensusNode(sdk.Bech32PrefixConsAddr, sdk.Bech32PrefixConsPub) 44 | config.Seal() 45 | 46 | cdc := app.MakeCodec() 47 | appCodec = cdc 48 | 49 | // Construct Root Command 50 | rootCmd.AddCommand( 51 | rpc.StatusCommand(), 52 | initRelayerCmd(), 53 | ) 54 | 55 | executor := cli.PrepareMainCmd(rootCmd, "EBRELAYER", defaultCLIHome) 56 | err := executor.Execute() 57 | if err != nil { 58 | panic(err) 59 | } 60 | } 61 | 62 | var rootCmd = &cobra.Command{ 63 | Use: "ebrelayer", 64 | Short: "Relayer service which listens for and relays ethereum smart contract events", 65 | SilenceUsage: true, 66 | } 67 | 68 | func initRelayerCmd() *cobra.Command { 69 | initRelayerCmd := &cobra.Command{ 70 | Use: "init chain-id web3-provider contract-address event-signature validatorFromName", 71 | Short: "Initalizes a web socket which streams live events from a smart contract", 72 | RunE: RunRelayerCmd, 73 | } 74 | 75 | return initRelayerCmd 76 | } 77 | 78 | func RunRelayerCmd(cmd *cobra.Command, args []string) error { 79 | if len(args) != 5 { 80 | return fmt.Errorf("Expected 5 arguments, got %v", len(args)) 81 | } 82 | 83 | // Parse chain's ID 84 | chainId := args[0] 85 | if chainId == "" { 86 | return fmt.Errorf("Invalid chain-id: %v", chainId) 87 | } 88 | 89 | // Parse ethereum provider 90 | ethereumProvider := args[1] 91 | if !relayer.IsWebsocketURL(ethereumProvider) { 92 | return fmt.Errorf("Invalid web3-provider: %v", ethereumProvider) 93 | } 94 | 95 | // Parse the address of the deployed contract 96 | bytesContractAddress, err := hex.DecodeString(args[2]) 97 | if err != nil { 98 | return fmt.Errorf("Invalid contract-address: %v", bytesContractAddress) 99 | } 100 | contractAddress := common.BytesToAddress(bytesContractAddress) 101 | 102 | // Parse the event signature for the subscription 103 | eventSig := "0xe154a56f2d306d5bbe4ac2379cb0cfc906b23685047a2bd2f5f0a0e810888f72" 104 | // eventSig := crypto.Keccak256Hash([]byte(args[3])) 105 | if eventSig == "" { 106 | return fmt.Errorf("Invalid event-signature: %v", eventSig) 107 | } 108 | 109 | // Parse the validator running the relayer service 110 | validatorFrom := args[4] 111 | 112 | // Initialize the relayer 113 | initErr := relayer.InitRelayer( 114 | appCodec, 115 | chainId, 116 | ethereumProvider, 117 | contractAddress, 118 | eventSig, 119 | validatorFrom) 120 | 121 | if initErr != nil { 122 | fmt.Printf("%v", initErr) 123 | return initErr 124 | } 125 | 126 | return nil 127 | } 128 | 129 | func main() { 130 | err := rootCmd.Execute() 131 | if err != nil { 132 | os.Exit(1) 133 | } 134 | os.Exit(0) 135 | } 136 | -------------------------------------------------------------------------------- /x/ethbridge/client/rest/rest.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/cosmos/cosmos-sdk/client/context" 9 | "github.com/cosmos/cosmos-sdk/types/rest" 10 | 11 | "github.com/cosmos/cosmos-sdk/codec" 12 | 13 | "github.com/gorilla/mux" 14 | 15 | clientrest "github.com/cosmos/cosmos-sdk/client/rest" 16 | sdk "github.com/cosmos/cosmos-sdk/types" 17 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge" 18 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/querier" 19 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/types" 20 | ) 21 | 22 | const ( 23 | restNonce = "nonce" 24 | restEthereumSender = "ethereumSender" 25 | ) 26 | 27 | // RegisterRoutes - Central function to define routes that get registered by the main application 28 | func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, queryRoute string) { 29 | r.HandleFunc(fmt.Sprintf("/%s/prophecies", queryRoute), makeClaimHandler(cdc, cliCtx)).Methods("POST") 30 | r.HandleFunc(fmt.Sprintf("/%s/prophecies/{%s}/{%s}", queryRoute, restNonce, restEthereumSender), getProphecyHandler(cdc, cliCtx, queryRoute)).Methods("GET") 31 | } 32 | 33 | type makeEthClaimReq struct { 34 | BaseReq rest.BaseReq `json:"base_req"` 35 | Nonce int `json:"nonce"` 36 | EthereumSender string `json:"ethereum_sender"` 37 | CosmosReceiver string `json:"cosmos_receiver"` 38 | Validator string `json:"validator"` 39 | Amount string `json:"amount"` 40 | } 41 | 42 | func makeClaimHandler(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc { 43 | return func(w http.ResponseWriter, r *http.Request) { 44 | var req makeEthClaimReq 45 | 46 | if !rest.ReadRESTReq(w, r, cdc, &req) { 47 | rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request") 48 | return 49 | } 50 | 51 | baseReq := req.BaseReq.Sanitize() 52 | if !baseReq.ValidateBasic(w) { 53 | return 54 | } 55 | 56 | ethereumSender := req.EthereumSender 57 | cosmosReceiver, err2 := sdk.AccAddressFromBech32(req.CosmosReceiver) 58 | if err2 != nil { 59 | rest.WriteErrorResponse(w, http.StatusBadRequest, err2.Error()) 60 | return 61 | } 62 | validator, err3 := sdk.AccAddressFromBech32(req.Validator) 63 | if err3 != nil { 64 | rest.WriteErrorResponse(w, http.StatusBadRequest, err3.Error()) 65 | return 66 | } 67 | 68 | amount, err4 := sdk.ParseCoins(req.Amount) 69 | if err4 != nil { 70 | rest.WriteErrorResponse(w, http.StatusBadRequest, err4.Error()) 71 | return 72 | } 73 | 74 | // create the message 75 | ethBridgeClaim := types.NewEthBridgeClaim(req.Nonce, ethereumSender, cosmosReceiver, validator, amount) 76 | msg := ethbridge.NewMsgMakeEthBridgeClaim(ethBridgeClaim) 77 | err5 := msg.ValidateBasic() 78 | if err5 != nil { 79 | rest.WriteErrorResponse(w, http.StatusBadRequest, err5.Error()) 80 | return 81 | } 82 | 83 | clientrest.WriteGenerateStdTxResponse(w, cdc, cliCtx, baseReq, []sdk.Msg{msg}) 84 | } 85 | } 86 | 87 | func getProphecyHandler(cdc *codec.Codec, cliCtx context.CLIContext, queryRoute string) http.HandlerFunc { 88 | return func(w http.ResponseWriter, r *http.Request) { 89 | vars := mux.Vars(r) 90 | nonce := vars[restNonce] 91 | 92 | nonceString, err := strconv.Atoi(nonce) 93 | if err != nil { 94 | rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) 95 | return 96 | } 97 | ethereumSender := vars[restEthereumSender] 98 | 99 | bz, err := cdc.MarshalJSON(ethbridge.NewQueryEthProphecyParams(nonceString, ethereumSender)) 100 | if err != nil { 101 | rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) 102 | return 103 | } 104 | 105 | route := fmt.Sprintf("custom/%s/%s", queryRoute, querier.QueryEthProphecy) 106 | res, err := cliCtx.QueryWithData(route, bz) 107 | if err != nil { 108 | rest.WriteErrorResponse(w, http.StatusNotFound, err.Error()) 109 | return 110 | } 111 | 112 | rest.PostProcessResponse(w, cdc, res, cliCtx.Indent) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /cmd/ebrelayer/relayer/relayer.go: -------------------------------------------------------------------------------- 1 | package relayer 2 | 3 | // ----------------------------------------------------- 4 | // Relayer 5 | // 6 | // Initializes the relayer service, which parses, 7 | // encodes, and packages named events on an Ethereum 8 | // Smart Contract for validator's to sign and send 9 | // to the Cosmos bridge. 10 | // ----------------------------------------------------- 11 | 12 | import ( 13 | "context" 14 | "fmt" 15 | "log" 16 | 17 | amino "github.com/tendermint/go-amino" 18 | 19 | sdkContext "github.com/cosmos/cosmos-sdk/client/context" 20 | "github.com/cosmos/cosmos-sdk/client/keys" 21 | authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" 22 | 23 | "github.com/ethereum/go-ethereum" 24 | "github.com/ethereum/go-ethereum/common" 25 | "github.com/ethereum/go-ethereum/core/types" 26 | 27 | "github.com/swishlabsco/cosmos-ethereum-bridge/cmd/ebrelayer/contract" 28 | "github.com/swishlabsco/cosmos-ethereum-bridge/cmd/ebrelayer/events" 29 | "github.com/swishlabsco/cosmos-ethereum-bridge/cmd/ebrelayer/txs" 30 | ) 31 | 32 | // ------------------------------------------------------------------------- 33 | // Starts an event listener on a specific network, contract, and event 34 | // ------------------------------------------------------------------------- 35 | 36 | func InitRelayer(cdc *amino.Codec, chainId string, provider string, 37 | contractAddress common.Address, eventSig string, 38 | validatorFrom string) error { 39 | 40 | validatorAddress, validatorName, err := sdkContext.GetFromFields(validatorFrom) 41 | if err != nil { 42 | fmt.Printf("failed to get from fields: %v", err) 43 | return err 44 | } 45 | 46 | passphrase, err := keys.GetPassphrase(validatorFrom) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | //Test passhprase is correct 52 | _, err = authtxb.MakeSignature(nil, validatorName, passphrase, authtxb.StdSignMsg{}) 53 | if err != nil { 54 | fmt.Printf("passphrase error: %v", err) 55 | return err 56 | } 57 | 58 | // Start client with infura ropsten provider 59 | client, err := SetupWebsocketEthClient(provider) 60 | if err != nil { 61 | fmt.Errorf("%s", err) 62 | } 63 | fmt.Printf("\nStarted ethereum websocket with provider: %s", provider) 64 | 65 | // We need the contract address in bytes[] for the query 66 | query := ethereum.FilterQuery{ 67 | Addresses: []common.Address{contractAddress}, 68 | } 69 | 70 | // We will check logs for new events 71 | logs := make(chan types.Log) 72 | 73 | // Filter by contract and event, write results to logs 74 | sub, err := client.SubscribeFilterLogs(context.Background(), query, logs) 75 | if err != nil { 76 | fmt.Errorf("%s", err) 77 | } else { 78 | fmt.Printf("\nSubscribed to contract events on address: %s\n", contractAddress.Hex()) 79 | } 80 | 81 | // Load Peggy Contract's ABI 82 | contractABI := contract.LoadABI() 83 | 84 | for { 85 | select { 86 | // Handle any errors 87 | case err := <-sub.Err(): 88 | log.Fatal(err) 89 | // vLog is raw event data 90 | case vLog := <-logs: 91 | // Check if the event is a 'LogLock' event 92 | if vLog.Topics[0].Hex() == eventSig { 93 | fmt.Printf("\n\nNew Lock Transaction:\nTx hash: %v\nBlock number: %v", 94 | vLog.TxHash.Hex(), vLog.BlockNumber) 95 | 96 | // Parse the event data into a new LockEvent using the contract's ABI 97 | event := events.NewLockEvent(contractABI, "LogLock", vLog.Data) 98 | 99 | // Add the event to the record 100 | successfulStore := events.NewEventWrite(vLog.TxHash.Hex(), event) 101 | if successfulStore != true { 102 | fmt.Errorf("Error: event not stored") 103 | } 104 | 105 | // Parse the event's payload into a struct 106 | claim, claimErr := txs.ParsePayload(validatorAddress, &event) 107 | if claimErr != nil { 108 | fmt.Errorf("Error: %s", claimErr) 109 | } 110 | 111 | // Initiate the relay 112 | relayErr := txs.RelayEvent(chainId, cdc, validatorAddress, validatorName, passphrase, &claim) 113 | if relayErr != nil { 114 | fmt.Errorf("Error: %s", relayErr) 115 | } 116 | } 117 | } 118 | } 119 | return fmt.Errorf("Error: Relayer timed out.") 120 | } 121 | -------------------------------------------------------------------------------- /export.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | 7 | abci "github.com/tendermint/tendermint/abci/types" 8 | tmtypes "github.com/tendermint/tendermint/types" 9 | 10 | "github.com/cosmos/cosmos-sdk/codec" 11 | sdk "github.com/cosmos/cosmos-sdk/types" 12 | "github.com/cosmos/cosmos-sdk/x/auth" 13 | "github.com/cosmos/cosmos-sdk/x/bank" 14 | "github.com/cosmos/cosmos-sdk/x/staking" 15 | ) 16 | 17 | // export the state of gaia for a genesis file 18 | func (app *ethereumBridgeApp) ExportAppStateAndValidators(forZeroHeight bool, jailWhiteList []string) ( 19 | appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) { 20 | 21 | // as if they could withdraw from the start of the next block 22 | ctx := app.NewContext(true, abci.Header{Height: app.LastBlockHeight()}) 23 | 24 | if forZeroHeight { 25 | app.prepForZeroHeightGenesis(ctx, jailWhiteList) 26 | } 27 | 28 | // iterate to get the accounts 29 | accounts := []GenesisAccount{} 30 | appendAccount := func(acc auth.Account) (stop bool) { 31 | account := NewGenesisAccountI(acc) 32 | accounts = append(accounts, account) 33 | return false 34 | } 35 | app.accountKeeper.IterateAccounts(ctx, appendAccount) 36 | 37 | genState := NewGenesisState( 38 | accounts, 39 | auth.ExportGenesis(ctx, app.accountKeeper, app.feeCollectionKeeper), 40 | bank.ExportGenesis(ctx, app.bankKeeper), 41 | staking.ExportGenesis(ctx, app.stakingKeeper), 42 | ) 43 | appState, err = codec.MarshalJSONIndent(app.cdc, genState) 44 | if err != nil { 45 | return nil, nil, err 46 | } 47 | validators = staking.WriteValidators(ctx, app.stakingKeeper) 48 | return appState, validators, nil 49 | } 50 | 51 | // prepare for fresh start at zero height 52 | func (app *ethereumBridgeApp) prepForZeroHeightGenesis(ctx sdk.Context, jailWhiteList []string) { 53 | applyWhiteList := false 54 | 55 | //Check if there is a whitelist 56 | if len(jailWhiteList) > 0 { 57 | applyWhiteList = true 58 | } 59 | 60 | whiteListMap := make(map[string]bool) 61 | 62 | for _, addr := range jailWhiteList { 63 | _, err := sdk.ValAddressFromBech32(addr) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | whiteListMap[addr] = true 68 | } 69 | 70 | // set context height to zero 71 | height := ctx.BlockHeight() 72 | ctx = ctx.WithBlockHeight(0) 73 | 74 | // reset context height 75 | ctx = ctx.WithBlockHeight(height) 76 | 77 | /* Handle staking state. */ 78 | 79 | // iterate through redelegations, reset creation height 80 | app.stakingKeeper.IterateRedelegations(ctx, func(_ int64, red staking.Redelegation) (stop bool) { 81 | for i := range red.Entries { 82 | red.Entries[i].CreationHeight = 0 83 | } 84 | app.stakingKeeper.SetRedelegation(ctx, red) 85 | return false 86 | }) 87 | 88 | // iterate through unbonding delegations, reset creation height 89 | app.stakingKeeper.IterateUnbondingDelegations(ctx, func(_ int64, ubd staking.UnbondingDelegation) (stop bool) { 90 | for i := range ubd.Entries { 91 | ubd.Entries[i].CreationHeight = 0 92 | } 93 | app.stakingKeeper.SetUnbondingDelegation(ctx, ubd) 94 | return false 95 | }) 96 | 97 | // Iterate through validators by power descending, reset bond heights, and 98 | // update bond intra-tx counters. 99 | store := ctx.KVStore(app.keyStaking) 100 | iter := sdk.KVStoreReversePrefixIterator(store, staking.ValidatorsKey) 101 | counter := int16(0) 102 | 103 | var valConsAddrs []sdk.ConsAddress 104 | for ; iter.Valid(); iter.Next() { 105 | addr := sdk.ValAddress(iter.Key()[1:]) 106 | validator, found := app.stakingKeeper.GetValidator(ctx, addr) 107 | if !found { 108 | panic("expected validator, not found") 109 | } 110 | 111 | validator.UnbondingHeight = 0 112 | valConsAddrs = append(valConsAddrs, validator.ConsAddress()) 113 | if applyWhiteList && !whiteListMap[addr.String()] { 114 | validator.Jailed = true 115 | } 116 | 117 | app.stakingKeeper.SetValidator(ctx, validator) 118 | counter++ 119 | } 120 | 121 | iter.Close() 122 | 123 | _ = app.stakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx) 124 | } 125 | -------------------------------------------------------------------------------- /cosmos-ethereum-bridge.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "cf767109-8494-42c9-8867-4af935a7a0c0", 4 | "name": "cosmos-ethereum-bridge", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "node: get endpoints", 10 | "request": { 11 | "method": "GET", 12 | "header": [], 13 | "body": { 14 | "mode": "raw", 15 | "raw": "" 16 | }, 17 | "url": { 18 | "raw": "http://localhost:26657", 19 | "protocol": "http", 20 | "host": [ 21 | "localhost" 22 | ], 23 | "port": "26657" 24 | } 25 | }, 26 | "response": [] 27 | }, 28 | { 29 | "name": "node: get status", 30 | "request": { 31 | "method": "POST", 32 | "header": [], 33 | "body": { 34 | "mode": "raw", 35 | "raw": "{\n \"method\": \"status\",\n \"jsonrpc\": \"2.0\",\n \"params\": [],\n \"id\": \"dontcare\"\n}\n" 36 | }, 37 | "url": { 38 | "raw": "http://localhost:26657", 39 | "protocol": "http", 40 | "host": [ 41 | "localhost" 42 | ], 43 | "port": "26657" 44 | } 45 | }, 46 | "response": [] 47 | }, 48 | { 49 | "name": "cli: version", 50 | "request": { 51 | "method": "GET", 52 | "header": [], 53 | "body": { 54 | "mode": "raw", 55 | "raw": "" 56 | }, 57 | "url": { 58 | "raw": "http://localhost:1317/node_info", 59 | "protocol": "http", 60 | "host": [ 61 | "localhost" 62 | ], 63 | "port": "1317", 64 | "path": [ 65 | "node_info" 66 | ] 67 | } 68 | }, 69 | "response": [] 70 | }, 71 | { 72 | "name": "cli: get account", 73 | "request": { 74 | "method": "GET", 75 | "header": [], 76 | "body": { 77 | "mode": "raw", 78 | "raw": "" 79 | }, 80 | "url": { 81 | "raw": "http://localhost:1317/auth/accounts/cosmos19l0hyjpzm8xkwlu84my4f0npd2ranxt2yfztux", 82 | "protocol": "http", 83 | "host": [ 84 | "localhost" 85 | ], 86 | "port": "1317", 87 | "path": [ 88 | "auth", 89 | "accounts", 90 | "cosmos19l0hyjpzm8xkwlu84my4f0npd2ranxt2yfztux" 91 | ] 92 | } 93 | }, 94 | "response": [] 95 | }, 96 | { 97 | "name": "cli: get account balances", 98 | "request": { 99 | "method": "GET", 100 | "header": [], 101 | "body": { 102 | "mode": "raw", 103 | "raw": "" 104 | }, 105 | "url": { 106 | "raw": "http://localhost:1317/bank/balances/cosmos1pjtgu0vau2m52nrykdpztrt887aykue0hq7dfh", 107 | "protocol": "http", 108 | "host": [ 109 | "localhost" 110 | ], 111 | "port": "1317", 112 | "path": [ 113 | "bank", 114 | "balances", 115 | "cosmos1pjtgu0vau2m52nrykdpztrt887aykue0hq7dfh" 116 | ] 117 | } 118 | }, 119 | "response": [] 120 | }, 121 | { 122 | "name": "cli: create eth prophecy", 123 | "request": { 124 | "method": "POST", 125 | "header": [ 126 | { 127 | "key": "Content-Type", 128 | "name": "Content-Type", 129 | "value": "application/json", 130 | "type": "text" 131 | } 132 | ], 133 | "body": { 134 | "mode": "raw", 135 | "raw": "{\n \"base_req\": {\n \"chain_id\": \"testing\",\n \"from\": \"cosmos18hf69vxn8a3tkladruxgxgv8tl8sl54gygdh29\"\n },\n \"nonce\": \"0\",\n \"ethereum_sender\": \"0x7B95B6EC7EbD73572298cEf32Bb54FA408207359\",\n \"amount\": \"4eth\",\n \"cosmos_receiver\": \"cosmos19l0hyjpzm8xkwlu84my4f0npd2ranxt2yfztux\"\n}" 136 | }, 137 | "url": { 138 | "raw": "http://localhost:1317/ethbridge/prophecies", 139 | "protocol": "http", 140 | "host": [ 141 | "localhost" 142 | ], 143 | "port": "1317", 144 | "path": [ 145 | "ethbridge", 146 | "prophecies" 147 | ] 148 | } 149 | }, 150 | "response": [] 151 | }, 152 | { 153 | "name": "cli: get prophecy", 154 | "request": { 155 | "method": "GET", 156 | "header": [], 157 | "body": { 158 | "mode": "raw", 159 | "raw": "" 160 | }, 161 | "url": { 162 | "raw": "http://localhost:1317/ethbridge/prophecies/0/0x7B95B6EC7EbD73572298cEf32Bb54FA408207359", 163 | "protocol": "http", 164 | "host": [ 165 | "localhost" 166 | ], 167 | "port": "1317", 168 | "path": [ 169 | "ethbridge", 170 | "prophecies", 171 | "0", 172 | "0x7B95B6EC7EbD73572298cEf32Bb54FA408207359" 173 | ] 174 | } 175 | }, 176 | "response": [] 177 | } 178 | ] 179 | } -------------------------------------------------------------------------------- /cmd/ebcli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | "github.com/cosmos/cosmos-sdk/client" 8 | "github.com/cosmos/cosmos-sdk/client/keys" 9 | "github.com/cosmos/cosmos-sdk/client/lcd" 10 | "github.com/cosmos/cosmos-sdk/client/rpc" 11 | "github.com/cosmos/cosmos-sdk/client/tx" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | amino "github.com/tendermint/go-amino" 15 | "github.com/tendermint/tendermint/libs/cli" 16 | 17 | sdk "github.com/cosmos/cosmos-sdk/types" 18 | authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" 19 | auth "github.com/cosmos/cosmos-sdk/x/auth/client/rest" 20 | bankcmd "github.com/cosmos/cosmos-sdk/x/bank/client/cli" 21 | bank "github.com/cosmos/cosmos-sdk/x/bank/client/rest" 22 | stakingModule "github.com/cosmos/cosmos-sdk/x/staking" 23 | 24 | stakingclient "github.com/cosmos/cosmos-sdk/x/staking/client" 25 | stakingrest "github.com/cosmos/cosmos-sdk/x/staking/client/rest" 26 | 27 | app "github.com/swishlabsco/cosmos-ethereum-bridge" 28 | ethbridgeclient "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/client" 29 | ethbridgerest "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/client/rest" 30 | ) 31 | 32 | const ( 33 | storeAcc = "acc" 34 | routeEthbridge = "ethbridge" 35 | ) 36 | 37 | var defaultCLIHome = os.ExpandEnv("$HOME/.ebcli") 38 | 39 | func main() { 40 | cobra.EnableCommandSorting = false 41 | 42 | cdc := app.MakeCodec() 43 | 44 | // Read in the configuration file for the sdk 45 | config := sdk.GetConfig() 46 | config.SetBech32PrefixForAccount(sdk.Bech32PrefixAccAddr, sdk.Bech32PrefixAccPub) 47 | config.SetBech32PrefixForValidator(sdk.Bech32PrefixValAddr, sdk.Bech32PrefixValPub) 48 | config.SetBech32PrefixForConsensusNode(sdk.Bech32PrefixConsAddr, sdk.Bech32PrefixConsPub) 49 | config.Seal() 50 | 51 | mc := []sdk.ModuleClients{ 52 | ethbridgeclient.NewModuleClient(routeEthbridge, cdc), 53 | stakingclient.NewModuleClient(stakingModule.StoreKey, cdc), 54 | } 55 | 56 | rootCmd := &cobra.Command{ 57 | Use: "ebcli", 58 | Short: "ethereum bridge Client", 59 | } 60 | 61 | // Add --chain-id to persistent flags and mark it required 62 | rootCmd.PersistentFlags().String(client.FlagChainID, "", "Chain ID of tendermint node") 63 | rootCmd.PersistentPreRunE = func(_ *cobra.Command, _ []string) error { 64 | return initConfig(rootCmd) 65 | } 66 | 67 | // Construct Root Command 68 | rootCmd.AddCommand( 69 | rpc.StatusCommand(), 70 | client.ConfigCmd(defaultCLIHome), 71 | queryCmd(cdc, mc), 72 | txCmd(cdc, mc), 73 | client.LineBreak, 74 | lcd.ServeCommand(cdc, registerRoutes), 75 | client.LineBreak, 76 | keys.Commands(), 77 | client.LineBreak, 78 | ) 79 | 80 | executor := cli.PrepareMainCmd(rootCmd, "EB", defaultCLIHome) 81 | err := executor.Execute() 82 | if err != nil { 83 | panic(err) 84 | } 85 | } 86 | 87 | func registerRoutes(rs *lcd.RestServer) { 88 | rs.CliCtx = rs.CliCtx.WithAccountDecoder(rs.Cdc) 89 | rpc.RegisterRoutes(rs.CliCtx, rs.Mux) 90 | tx.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc) 91 | auth.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, storeAcc) 92 | bank.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, rs.KeyBase) 93 | stakingrest.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, rs.KeyBase) 94 | ethbridgerest.RegisterRoutes(rs.CliCtx, rs.Mux, rs.Cdc, routeEthbridge) 95 | } 96 | 97 | func queryCmd(cdc *amino.Codec, mc []sdk.ModuleClients) *cobra.Command { 98 | queryCmd := &cobra.Command{ 99 | Use: "query", 100 | Aliases: []string{"q"}, 101 | Short: "Querying subcommands", 102 | } 103 | 104 | queryCmd.AddCommand( 105 | rpc.ValidatorCommand(cdc), 106 | rpc.BlockCommand(), 107 | tx.SearchTxCmd(cdc), 108 | tx.QueryTxCmd(cdc), 109 | client.LineBreak, 110 | authcmd.GetAccountCmd(storeAcc, cdc), 111 | ) 112 | 113 | for _, m := range mc { 114 | mQueryCmd := m.GetQueryCmd() 115 | if mQueryCmd != nil { 116 | queryCmd.AddCommand(mQueryCmd) 117 | } 118 | } 119 | 120 | return queryCmd 121 | } 122 | 123 | func txCmd(cdc *amino.Codec, mc []sdk.ModuleClients) *cobra.Command { 124 | txCmd := &cobra.Command{ 125 | Use: "tx", 126 | Short: "Transactions subcommands", 127 | } 128 | 129 | txCmd.AddCommand( 130 | bankcmd.SendTxCmd(cdc), 131 | client.LineBreak, 132 | authcmd.GetSignCommand(cdc), 133 | tx.GetBroadcastCommand(cdc), 134 | client.LineBreak, 135 | ) 136 | 137 | for _, m := range mc { 138 | txCmd.AddCommand(m.GetTxCmd()) 139 | } 140 | 141 | return txCmd 142 | } 143 | 144 | func initConfig(cmd *cobra.Command) error { 145 | home, err := cmd.PersistentFlags().GetString(cli.HomeFlag) 146 | if err != nil { 147 | return err 148 | } 149 | 150 | cfgFile := path.Join(home, "config", "config.toml") 151 | if _, err := os.Stat(cfgFile); err == nil { 152 | viper.SetConfigFile(cfgFile) 153 | 154 | if err := viper.ReadInConfig(); err != nil { 155 | return err 156 | } 157 | } 158 | if err := viper.BindPFlag(client.FlagChainID, cmd.PersistentFlags().Lookup(client.FlagChainID)); err != nil { 159 | return err 160 | } 161 | if err := viper.BindPFlag(cli.EncodingFlag, cmd.PersistentFlags().Lookup(cli.EncodingFlag)); err != nil { 162 | return err 163 | } 164 | return viper.BindPFlag(cli.OutputFlag, cmd.PersistentFlags().Lookup(cli.OutputFlag)) 165 | } 166 | -------------------------------------------------------------------------------- /x/oracle/keeper/keeper.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "github.com/cosmos/cosmos-sdk/codec" 5 | "github.com/cosmos/cosmos-sdk/x/bank" 6 | "github.com/cosmos/cosmos-sdk/x/staking" 7 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle/types" 8 | 9 | sdk "github.com/cosmos/cosmos-sdk/types" 10 | ) 11 | 12 | // Keeper maintains the link to data storage and exposes getter/setter methods for the various parts of the state machine 13 | type Keeper struct { 14 | coinKeeper bank.Keeper 15 | stakeKeeper staking.Keeper 16 | 17 | storeKey sdk.StoreKey // Unexposed key to access store from sdk.Context 18 | 19 | cdc *codec.Codec // The wire codec for binary encoding/decoding. 20 | 21 | codespace sdk.CodespaceType 22 | 23 | consensusNeeded float64 24 | } 25 | 26 | // NewKeeper creates new instances of the oracle Keeper 27 | func NewKeeper(stakeKeeper staking.Keeper, storeKey sdk.StoreKey, cdc *codec.Codec, codespace sdk.CodespaceType, consensusNeeded float64) (Keeper, sdk.Error) { 28 | if consensusNeeded <= 0 || consensusNeeded > 1 { 29 | return Keeper{}, types.ErrMinimumConsensusNeededInvalid(codespace) 30 | } 31 | return Keeper{ 32 | stakeKeeper: stakeKeeper, 33 | storeKey: storeKey, 34 | cdc: cdc, 35 | codespace: codespace, 36 | consensusNeeded: consensusNeeded, 37 | }, nil 38 | } 39 | 40 | // Codespace returns the codespace 41 | func (k Keeper) Codespace() sdk.CodespaceType { 42 | return k.codespace 43 | } 44 | 45 | // GetProphecy gets the entire prophecy data struct for a given id 46 | func (k Keeper) GetProphecy(ctx sdk.Context, id string) (types.Prophecy, sdk.Error) { 47 | if id == "" { 48 | return types.NewEmptyProphecy(), types.ErrInvalidIdentifier(k.Codespace()) 49 | } 50 | store := ctx.KVStore(k.storeKey) 51 | if !store.Has([]byte(id)) { 52 | return types.NewEmptyProphecy(), types.ErrProphecyNotFound(k.Codespace()) 53 | } 54 | bz := store.Get([]byte(id)) 55 | var dbProphecy types.DBProphecy 56 | k.cdc.MustUnmarshalBinaryBare(bz, &dbProphecy) 57 | 58 | deSerializedProphecy, err := dbProphecy.DeserializeFromDB() 59 | if err != nil { 60 | return types.NewEmptyProphecy(), types.ErrInternalDB(k.Codespace(), err) 61 | } 62 | return deSerializedProphecy, nil 63 | } 64 | 65 | // saveProphecy saves a prophecy with an initial claim 66 | func (k Keeper) saveProphecy(ctx sdk.Context, prophecy types.Prophecy) sdk.Error { 67 | if prophecy.ID == "" { 68 | return types.ErrInvalidIdentifier(k.Codespace()) 69 | } 70 | if len(prophecy.ClaimValidators) <= 0 { 71 | return types.ErrNoClaims(k.Codespace()) 72 | } 73 | store := ctx.KVStore(k.storeKey) 74 | serializedProphecy, err := prophecy.SerializeForDB() 75 | if err != nil { 76 | return types.ErrInternalDB(k.Codespace(), err) 77 | } 78 | store.Set([]byte(prophecy.ID), k.cdc.MustMarshalBinaryBare(serializedProphecy)) 79 | return nil 80 | } 81 | 82 | func (k Keeper) ProcessClaim(ctx sdk.Context, id string, validator sdk.ValAddress, claim string) (types.Status, sdk.Error) { 83 | activeValidator := k.checkActiveValidator(ctx, validator) 84 | if !activeValidator { 85 | return types.Status{}, types.ErrInvalidValidator(k.Codespace()) 86 | } 87 | if claim == "" { 88 | return types.Status{}, types.ErrInvalidClaim(k.Codespace()) 89 | } 90 | prophecy, err := k.GetProphecy(ctx, id) 91 | if err == nil { 92 | if prophecy.Status.StatusText == types.SuccessStatusText || prophecy.Status.StatusText == types.FailedStatusText { 93 | return types.Status{}, types.ErrProphecyFinalized(k.Codespace()) 94 | } 95 | if prophecy.ValidatorClaims[validator.String()] != "" { 96 | return types.Status{}, types.ErrDuplicateMessage(k.Codespace()) 97 | } 98 | prophecy.AddClaim(validator, claim) 99 | } else { 100 | if err.Code() != types.CodeProphecyNotFound { 101 | return types.Status{}, err 102 | } 103 | prophecy = types.NewProphecy(id) 104 | prophecy.AddClaim(validator, claim) 105 | } 106 | prophecy = k.processCompletion(ctx, prophecy) 107 | err = k.saveProphecy(ctx, prophecy) 108 | if err != nil { 109 | return types.Status{}, err 110 | } 111 | return prophecy.Status, nil 112 | } 113 | 114 | func (k Keeper) checkActiveValidator(ctx sdk.Context, validatorAddress sdk.ValAddress) bool { 115 | validator, found := k.stakeKeeper.GetValidator(ctx, validatorAddress) 116 | if !found { 117 | return false 118 | } 119 | bondStatus := validator.GetStatus() 120 | if bondStatus != sdk.Bonded { 121 | return false 122 | } 123 | return true 124 | } 125 | 126 | func (k Keeper) processCompletion(ctx sdk.Context, prophecy types.Prophecy) types.Prophecy { 127 | highestClaim, highestClaimPower, totalClaimsPower := prophecy.FindHighestClaim(ctx, k.stakeKeeper) 128 | totalPower := k.stakeKeeper.GetLastTotalPower(ctx) 129 | highestConsensusRatio := float64(highestClaimPower) / float64(totalPower.Int64()) 130 | remainingPossibleClaimPower := totalPower.Int64() - totalClaimsPower 131 | highestPossibleClaimPower := highestClaimPower + remainingPossibleClaimPower 132 | highestPossibleConsensusRatio := float64(highestPossibleClaimPower) / float64(totalPower.Int64()) 133 | if highestConsensusRatio >= k.consensusNeeded { 134 | prophecy.Status.StatusText = types.SuccessStatusText 135 | prophecy.Status.FinalClaim = highestClaim 136 | } else if highestPossibleConsensusRatio <= k.consensusNeeded { 137 | prophecy.Status.StatusText = types.FailedStatusText 138 | } 139 | return prophecy 140 | } 141 | -------------------------------------------------------------------------------- /ethereum-contracts/contracts/Processor.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "openzeppelin-solidity/contracts/math/SafeMath.sol"; 4 | import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; 5 | 6 | /* 7 | * @title: Processor 8 | * @dev: Processes requests for item locking and unlocking by 9 | * storing an item's information then relaying the funds 10 | * the original sender. 11 | */ 12 | contract Processor { 13 | 14 | using SafeMath for uint256; 15 | 16 | /* 17 | * @dev: Item struct to store information. 18 | */ 19 | struct Item { 20 | address payable sender; 21 | bytes recipient; 22 | address token; 23 | uint256 amount; 24 | uint256 nonce; 25 | bool locked; 26 | } 27 | 28 | uint256 public nonce; 29 | mapping(bytes32 => Item) private items; 30 | 31 | /* 32 | * @dev: Constructor, initalizes item count. 33 | */ 34 | constructor() 35 | public 36 | { 37 | nonce = 0; 38 | } 39 | 40 | modifier onlySender(bytes32 _id) { 41 | require( 42 | msg.sender == items[_id].sender, 43 | 'Must be the original sender.' 44 | ); 45 | _; 46 | } 47 | 48 | modifier canDeliver(bytes32 _id) { 49 | if(items[_id].token == address(0)) { 50 | require( 51 | address(this).balance >= items[_id].amount, 52 | 'Insufficient ethereum balance for delivery.' 53 | ); 54 | } else { 55 | require( 56 | ERC20(items[_id].token).balanceOf(address(this)) >= items[_id].amount, 57 | 'Insufficient ERC20 token balance for delivery.' 58 | ); 59 | } 60 | _; 61 | } 62 | 63 | modifier availableNonce() { 64 | require( 65 | nonce + 1 > nonce, 66 | 'No available nonces.' 67 | ); 68 | _; 69 | } 70 | 71 | /* 72 | * @dev: Creates an item with a unique id. 73 | * 74 | * @param _sender: The sender's ethereum address. 75 | * @param _recipient: The intended recipient's cosmos address. 76 | * @param _token: The currency type, either erc20 or ethereum. 77 | * @param _amount: The amount of erc20 tokens/ ethereum (in wei) to be itemized. 78 | * @return: The newly created item's unique id. 79 | */ 80 | function create( 81 | address payable _sender, 82 | bytes memory _recipient, 83 | address _token, 84 | uint256 _amount 85 | ) 86 | internal 87 | returns(bytes32) 88 | { 89 | nonce++; 90 | 91 | bytes32 itemKey = keccak256( 92 | abi.encodePacked( 93 | _sender, 94 | _recipient, 95 | _token, 96 | _amount, 97 | nonce 98 | ) 99 | ); 100 | 101 | items[itemKey] = Item( 102 | _sender, 103 | _recipient, 104 | _token, 105 | _amount, 106 | nonce, 107 | true 108 | ); 109 | 110 | return itemKey; 111 | } 112 | 113 | /* 114 | * @dev: Completes the item by sending the funds to the 115 | * original sender and unlocking the item. 116 | * 117 | * @param _id: The item to be completed. 118 | */ 119 | function complete( 120 | bytes32 _id 121 | ) 122 | internal 123 | canDeliver(_id) 124 | returns(address payable, address, uint256, uint256) 125 | { 126 | require(isLocked(_id)); 127 | 128 | //Get locked item's attributes for return 129 | address payable sender = items[_id].sender; 130 | address token = items[_id].token; 131 | uint256 amount = items[_id].amount; 132 | uint256 uniqueNonce = items[_id].nonce; 133 | 134 | //Update lock status 135 | items[_id].locked = false; 136 | 137 | //Transfers based on token address type 138 | if (token == address(0)) { 139 | sender.transfer(amount); 140 | } else { 141 | require(ERC20(token).transfer(sender, amount)); 142 | } 143 | 144 | return(sender, token, amount, uniqueNonce); 145 | } 146 | 147 | /* 148 | * @dev: Checks the current nonce. 149 | * 150 | * @return: The current nonce. 151 | */ 152 | function getNonce() 153 | internal 154 | view 155 | returns(uint256) 156 | { 157 | return nonce; 158 | } 159 | 160 | /* 161 | * @dev: Checks if an individual item exists. 162 | * 163 | * @param _id: The unique item's id. 164 | * @return: Boolean indicating if the item exists in memory. 165 | */ 166 | function isLocked( 167 | bytes32 _id 168 | ) 169 | internal 170 | view 171 | returns(bool) 172 | { 173 | return(items[_id].locked); 174 | } 175 | 176 | /* 177 | * @dev: Gets an item's information 178 | * 179 | * @param _Id: The item containing the desired information. 180 | * @return: Sender's address. 181 | * @return: Recipient's address in bytes. 182 | * @return: Token address. 183 | * @return: Amount of ethereum/erc20 in the item. 184 | * @return: Unique nonce of the item. 185 | */ 186 | function getItem( 187 | bytes32 _id 188 | ) 189 | internal 190 | view 191 | returns(address payable, bytes memory, address, uint256, uint256) 192 | { 193 | Item memory item = items[_id]; 194 | 195 | return( 196 | item.sender, 197 | item.recipient, 198 | item.token, 199 | item.amount, 200 | item.nonce 201 | ); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /x/oracle/types/prophecy.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/cosmos/cosmos-sdk/x/staking" 7 | 8 | sdk "github.com/cosmos/cosmos-sdk/types" 9 | ) 10 | 11 | const PendingStatusText = "pending" 12 | const SuccessStatusText = "success" 13 | const FailedStatusText = "failed" 14 | 15 | // Prophecy is a struct that contains all the metadata of an oracle ritual. 16 | // Claims are indexed by the claim's validator bech32 address and by the claim's json value to allow 17 | // for constant lookup times for any validation/verifiation checks of duplicate claims 18 | // Each transaction, pending potential results are also calculated, stored and indexed by their byte result 19 | // to allow discovery of consensus on any the result in constant time without having to sort or run 20 | // through the list of claims to find the one with highest consensus 21 | 22 | type Prophecy struct { 23 | ID string `json:"id"` 24 | Status Status `json:"status"` 25 | ClaimValidators map[string][]sdk.ValAddress `json:"claim_validators"` //This is a mapping from a claim to the list of validators that made that claim 26 | ValidatorClaims map[string]string `json:"validator_claims"` //This is a mapping from a validator bech32 address to their claim 27 | } 28 | 29 | // DBProphecy is what the prophecy becomes when being saved to the database. Tendermint/Amino does not support maps so we must serialize those variables into bytes. 30 | type DBProphecy struct { 31 | ID string `json:"id"` 32 | Status Status `json:"status"` 33 | ClaimValidators []byte `json:"claim_validators"` //This is a mapping from a claim to the list of validators that made that claim 34 | ValidatorClaims []byte `json:"validator_claims"` //This is a mapping from a validator bech32 address to their claim 35 | } 36 | 37 | // SerializeForDB serializes a prophecy into a DBProphecy 38 | // TODO: Using gob here may mean that different tendermint clients in different languages may serialize/store 39 | // prophecies in their db in different ways - check with @codereviewer if this is ok or if it introduces a risk of creating forks. 40 | // Or maybe using a slower json serializer or Amino:JSON would be ok 41 | func (prophecy Prophecy) SerializeForDB() (DBProphecy, error) { 42 | claimValidators, err := json.Marshal(prophecy.ClaimValidators) 43 | if err != nil { 44 | return DBProphecy{}, err 45 | } 46 | 47 | validatorClaims, err := json.Marshal(prophecy.ValidatorClaims) 48 | if err != nil { 49 | return DBProphecy{}, err 50 | } 51 | 52 | return DBProphecy{ 53 | ID: prophecy.ID, 54 | Status: prophecy.Status, 55 | ClaimValidators: claimValidators, 56 | ValidatorClaims: validatorClaims, 57 | }, nil 58 | } 59 | 60 | // DeserializeFromDB deserializes a DBProphecy into a prophecy 61 | func (dbProphecy DBProphecy) DeserializeFromDB() (Prophecy, error) { 62 | var claimValidators map[string][]sdk.ValAddress 63 | err := json.Unmarshal(dbProphecy.ClaimValidators, &claimValidators) 64 | if err != nil { 65 | return Prophecy{}, err 66 | } 67 | 68 | var validatorClaims map[string]string 69 | err = json.Unmarshal(dbProphecy.ValidatorClaims, &validatorClaims) 70 | if err != nil { 71 | return Prophecy{}, err 72 | } 73 | 74 | return Prophecy{ 75 | ID: dbProphecy.ID, 76 | Status: dbProphecy.Status, 77 | ClaimValidators: claimValidators, 78 | ValidatorClaims: validatorClaims, 79 | }, nil 80 | } 81 | 82 | // AddClaim adds a given claim to this prophecy 83 | func (prophecy Prophecy) AddClaim(validator sdk.ValAddress, claim string) { 84 | claimValidators := prophecy.ClaimValidators[claim] 85 | prophecy.ClaimValidators[claim] = append(claimValidators, validator) 86 | 87 | validatorBech32 := validator.String() 88 | prophecy.ValidatorClaims[validatorBech32] = claim 89 | } 90 | 91 | func (prophecy Prophecy) FindHighestClaim(ctx sdk.Context, stakeKeeper staking.Keeper) (string, int64, int64) { 92 | validators := stakeKeeper.GetBondedValidatorsByPower(ctx) 93 | //Index the validators by address for looking when scanning through claims 94 | validatorsByAddress := make(map[string]staking.Validator) 95 | for _, validator := range validators { 96 | validatorsByAddress[validator.OperatorAddress.String()] = validator 97 | } 98 | 99 | totalClaimsPower := int64(0) 100 | highestClaimPower := int64(-1) 101 | highestClaim := "" 102 | for claim, validators := range prophecy.ClaimValidators { 103 | claimPower := int64(0) 104 | for _, validator := range validators { 105 | validatorPower := validatorsByAddress[validator.String()].GetTendermintPower() 106 | claimPower += validatorPower 107 | } 108 | totalClaimsPower += claimPower 109 | if claimPower > highestClaimPower { 110 | highestClaimPower = claimPower 111 | highestClaim = claim 112 | } 113 | } 114 | return highestClaim, highestClaimPower, totalClaimsPower 115 | } 116 | 117 | // NewProphecy returns a new Prophecy, initialized in pending status with an initial claim 118 | func NewProphecy(id string) Prophecy { 119 | return Prophecy{ 120 | ID: id, 121 | Status: NewStatus(PendingStatusText, ""), 122 | ClaimValidators: make(map[string][]sdk.ValAddress), 123 | ValidatorClaims: make(map[string]string), 124 | } 125 | } 126 | 127 | // NewEmptyProphecy returns a blank prophecy, used with errors 128 | func NewEmptyProphecy() Prophecy { 129 | return NewProphecy("") 130 | } 131 | 132 | // Status is a struct that contains the status of a given prophecy 133 | type Status struct { 134 | StatusText string `json:"status_text"` 135 | FinalClaim string `json:"final_claim"` 136 | } 137 | 138 | // NewStatus returns a new Status with the given data contained 139 | func NewStatus(status string, finalClaim string) Status { 140 | return Status{ 141 | StatusText: status, 142 | FinalClaim: finalClaim, 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /x/ethbridge/handler_test.go: -------------------------------------------------------------------------------- 1 | package ethbridge 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle" 8 | 9 | "github.com/cosmos/cosmos-sdk/codec" 10 | sdk "github.com/cosmos/cosmos-sdk/types" 11 | 12 | "github.com/stretchr/testify/require" 13 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge/types" 14 | keeperLib "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle/keeper" 15 | ) 16 | 17 | func TestBasicMsgs(t *testing.T) { 18 | //Setup 19 | cdc := codec.New() 20 | ctx, _, keeper, bankKeeper, validatorAddresses, _ := keeperLib.CreateTestKeepers(t, 0.7, []int64{3, 7}) 21 | accAddress := sdk.AccAddress(validatorAddresses[0]) 22 | 23 | handler := NewHandler(keeper, bankKeeper, cdc, types.DefaultCodespace) 24 | 25 | //Unrecognized type 26 | res := handler(ctx, sdk.NewTestMsg()) 27 | require.False(t, res.IsOK()) 28 | require.True(t, strings.Contains(res.Log, "Unrecognized ethbridge message type: ")) 29 | 30 | //Normal Creation 31 | normalCreateMsg := types.CreateTestEthMsg(t, accAddress) 32 | res = handler(ctx, normalCreateMsg) 33 | require.True(t, res.IsOK()) 34 | 35 | //Bad Creation 36 | badCreateMsg := types.CreateTestEthMsg(t, accAddress) 37 | badCreateMsg.Nonce = -1 38 | res = handler(ctx, badCreateMsg) 39 | require.False(t, res.IsOK()) 40 | require.True(t, strings.Contains(res.Log, "invalid ethereum nonce provided")) 41 | 42 | badCreateMsg = types.CreateTestEthMsg(t, accAddress) 43 | badCreateMsg.EthereumSender = "badAddress" 44 | res = handler(ctx, badCreateMsg) 45 | require.False(t, res.IsOK()) 46 | require.True(t, strings.Contains(res.Log, "invalid ethereum address provided")) 47 | } 48 | 49 | func TestDuplicateMsgs(t *testing.T) { 50 | cdc := codec.New() 51 | ctx, _, keeper, bankKeeper, validatorAddresses, _ := keeperLib.CreateTestKeepers(t, 0.7, []int64{3, 7}) 52 | accAddress := sdk.AccAddress(validatorAddresses[0]) 53 | 54 | handler := NewHandler(keeper, bankKeeper, cdc, types.DefaultCodespace) 55 | normalCreateMsg := types.CreateTestEthMsg(t, accAddress) 56 | res := handler(ctx, normalCreateMsg) 57 | require.True(t, res.IsOK()) 58 | require.Equal(t, res.Log, oracle.PendingStatus) 59 | 60 | //Duplicate message from same validator 61 | res = handler(ctx, normalCreateMsg) 62 | require.False(t, res.IsOK()) 63 | require.True(t, strings.Contains(res.Log, "Already processed message from validator for this id")) 64 | 65 | } 66 | 67 | func TestMintSuccess(t *testing.T) { 68 | //Setup 69 | cdc := codec.New() 70 | ctx, _, keeper, bankKeeper, validatorAddresses, _ := keeperLib.CreateTestKeepers(t, 0.7, []int64{2, 7, 1}) 71 | accAddressVal1Pow2 := sdk.AccAddress(validatorAddresses[0]) 72 | accAddressVal2Pow7 := sdk.AccAddress(validatorAddresses[1]) 73 | accAddressVal3Pow1 := sdk.AccAddress(validatorAddresses[2]) 74 | 75 | handler := NewHandler(keeper, bankKeeper, cdc, types.DefaultCodespace) 76 | 77 | //Initial message 78 | normalCreateMsg := types.CreateTestEthMsg(t, accAddressVal1Pow2) 79 | res := handler(ctx, normalCreateMsg) 80 | require.True(t, res.IsOK()) 81 | 82 | //Message from second validator succeeds and mints new tokens 83 | normalCreateMsg = types.CreateTestEthMsg(t, accAddressVal2Pow7) 84 | res = handler(ctx, normalCreateMsg) 85 | require.True(t, res.IsOK()) 86 | receiverAddress, err := sdk.AccAddressFromBech32(types.TestAddress) 87 | require.NoError(t, err) 88 | receiverCoins := bankKeeper.GetCoins(ctx, receiverAddress) 89 | expectedCoins, err := sdk.ParseCoins(types.TestCoins) 90 | require.NoError(t, err) 91 | require.True(t, receiverCoins.IsEqual(expectedCoins)) 92 | require.Equal(t, res.Log, oracle.SuccessStatus) 93 | 94 | //Additional message from third validator fails and does not mint 95 | normalCreateMsg = types.CreateTestEthMsg(t, accAddressVal3Pow1) 96 | res = handler(ctx, normalCreateMsg) 97 | require.False(t, res.IsOK()) 98 | require.True(t, strings.Contains(res.Log, "Prophecy already finalized")) 99 | receiverCoins = bankKeeper.GetCoins(ctx, receiverAddress) 100 | expectedCoins, err = sdk.ParseCoins(types.TestCoins) 101 | require.NoError(t, err) 102 | require.True(t, receiverCoins.IsEqual(expectedCoins)) 103 | 104 | } 105 | 106 | func TestNoMintFail(t *testing.T) { 107 | //Setup 108 | cdc := codec.New() 109 | ctx, _, keeper, bankKeeper, validatorAddresses, _ := keeperLib.CreateTestKeepers(t, 0.7, []int64{3, 4, 3}) 110 | accAddressVal1Pow3 := sdk.AccAddress(validatorAddresses[0]) 111 | accAddressVal2Pow4 := sdk.AccAddress(validatorAddresses[1]) 112 | accAddressVal3Pow3 := sdk.AccAddress(validatorAddresses[2]) 113 | 114 | ethClaim1 := types.CreateTestEthClaim(t, accAddressVal1Pow3, types.TestEthereumAddress, types.TestCoins) 115 | ethMsg1 := NewMsgMakeEthBridgeClaim(ethClaim1) 116 | ethClaim2 := types.CreateTestEthClaim(t, accAddressVal2Pow4, types.AltTestEthereumAddress, types.TestCoins) 117 | ethMsg2 := NewMsgMakeEthBridgeClaim(ethClaim2) 118 | ethClaim3 := types.CreateTestEthClaim(t, accAddressVal3Pow3, types.TestEthereumAddress, types.AltTestCoins) 119 | ethMsg3 := NewMsgMakeEthBridgeClaim(ethClaim3) 120 | 121 | handler := NewHandler(keeper, bankKeeper, cdc, types.DefaultCodespace) 122 | 123 | //Initial message 124 | res := handler(ctx, ethMsg1) 125 | require.True(t, res.IsOK()) 126 | require.True(t, strings.Contains(res.Log, oracle.PendingStatus)) 127 | require.Equal(t, res.Log, oracle.PendingStatus) 128 | 129 | //Different message from second validator succeeds 130 | res = handler(ctx, ethMsg2) 131 | require.True(t, res.IsOK()) 132 | require.True(t, strings.Contains(res.Log, oracle.PendingStatus)) 133 | require.Equal(t, res.Log, oracle.PendingStatus) 134 | 135 | //Different message from third validator succeeds but results in failed prophecy with no minting 136 | res = handler(ctx, ethMsg3) 137 | require.True(t, res.IsOK()) 138 | require.True(t, strings.Contains(res.Log, oracle.FailedStatus)) 139 | receiverAddress, err := sdk.AccAddressFromBech32(types.TestAddress) 140 | require.NoError(t, err) 141 | receiver1Coins := bankKeeper.GetCoins(ctx, receiverAddress) 142 | require.True(t, receiver1Coins.IsZero()) 143 | } 144 | -------------------------------------------------------------------------------- /x/oracle/keeper/test_common.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle/types" 10 | "github.com/tendermint/tendermint/crypto" 11 | "github.com/tendermint/tendermint/libs/log" 12 | 13 | "github.com/cosmos/cosmos-sdk/codec" 14 | "github.com/cosmos/cosmos-sdk/store" 15 | cmtypes "github.com/cosmos/cosmos-sdk/types" 16 | sdk "github.com/cosmos/cosmos-sdk/types" 17 | "github.com/cosmos/cosmos-sdk/x/auth" 18 | "github.com/cosmos/cosmos-sdk/x/bank" 19 | "github.com/cosmos/cosmos-sdk/x/params" 20 | "github.com/cosmos/cosmos-sdk/x/staking" 21 | stakingKeeperLib "github.com/cosmos/cosmos-sdk/x/staking/keeper" 22 | abci "github.com/tendermint/tendermint/abci/types" 23 | dbm "github.com/tendermint/tendermint/libs/db" 24 | tmtypes "github.com/tendermint/tendermint/types" 25 | ) 26 | 27 | // CreateTestKeepers greates an OracleKeeper, AccountKeeper and Context to be used for test input 28 | func CreateTestKeepers(t *testing.T, consensusNeeded float64, validatorPowers []int64) (sdk.Context, auth.AccountKeeper, Keeper, bank.Keeper, []sdk.ValAddress, sdk.Error) { 29 | keyOracle := sdk.NewKVStoreKey(types.StoreKey) 30 | keyAcc := sdk.NewKVStoreKey(auth.StoreKey) 31 | keyParams := sdk.NewKVStoreKey(params.StoreKey) 32 | tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey) 33 | keyStaking := sdk.NewKVStoreKey(staking.StoreKey) 34 | tkeyStaking := sdk.NewTransientStoreKey(staking.TStoreKey) 35 | 36 | db := dbm.NewMemDB() 37 | ms := store.NewCommitMultiStore(db) 38 | ms.MountStoreWithDB(tkeyStaking, sdk.StoreTypeTransient, nil) 39 | ms.MountStoreWithDB(keyStaking, sdk.StoreTypeIAVL, db) 40 | ms.MountStoreWithDB(keyOracle, sdk.StoreTypeIAVL, db) 41 | ms.MountStoreWithDB(keyAcc, sdk.StoreTypeIAVL, db) 42 | ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db) 43 | ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db) 44 | err := ms.LoadLatestVersion() 45 | require.Nil(t, err) 46 | 47 | ctx := sdk.NewContext(ms, abci.Header{ChainID: "testchainid"}, false, log.NewNopLogger()) 48 | ctx = ctx.WithConsensusParams( 49 | &abci.ConsensusParams{ 50 | Validator: &abci.ValidatorParams{ 51 | PubKeyTypes: []string{tmtypes.ABCIPubKeyTypeEd25519}, 52 | }, 53 | }, 54 | ) 55 | cdc := MakeTestCodec() 56 | 57 | pk := params.NewKeeper(cdc, keyParams, tkeyParams) 58 | 59 | accountKeeper := auth.NewAccountKeeper( 60 | cdc, // amino codec 61 | keyAcc, // target store 62 | pk.Subspace(auth.DefaultParamspace), 63 | auth.ProtoBaseAccount, // prototype 64 | ) 65 | 66 | bankKeeper := bank.NewBaseKeeper( 67 | accountKeeper, 68 | pk.Subspace(bank.DefaultParamspace), 69 | bank.DefaultCodespace, 70 | ) 71 | 72 | stakingKeeper := staking.NewKeeper(cdc, keyStaking, tkeyStaking, bankKeeper, pk.Subspace(staking.DefaultParamspace), staking.DefaultCodespace) 73 | stakingKeeper.SetPool(ctx, staking.InitialPool()) 74 | stakingKeeper.SetParams(ctx, staking.DefaultParams()) 75 | 76 | keeper, keeperErr := NewKeeper(stakingKeeper, keyOracle, cdc, types.DefaultCodespace, consensusNeeded) 77 | 78 | //construct the validators 79 | numValidators := len(validatorPowers) 80 | validators := make([]staking.Validator, numValidators) 81 | accountAddresses, valAddresses := CreateTestAddrs(len(validatorPowers)) 82 | publicKeys := createTestPubKeys(len(validatorPowers)) 83 | 84 | // create the validators addresses desired and fill them with the expected amount of coins 85 | for i, power := range validatorPowers { 86 | coins := cmtypes.TokensFromTendermintPower(power) 87 | pool := stakingKeeper.GetPool(ctx) 88 | err := error(nil) 89 | _, _, err = bankKeeper.AddCoins(ctx, accountAddresses[i], sdk.Coins{ 90 | {stakingKeeper.BondDenom(ctx), coins}, 91 | }) 92 | require.Nil(t, err) 93 | pool.NotBondedTokens = pool.NotBondedTokens.Add(coins) 94 | stakingKeeper.SetPool(ctx, pool) 95 | } 96 | pool := stakingKeeper.GetPool(ctx) 97 | for i, power := range validatorPowers { 98 | validators[i] = staking.NewValidator(valAddresses[i], publicKeys[i], staking.Description{}) 99 | validators[i].Status = sdk.Bonded 100 | validators[i].Tokens = sdk.ZeroInt() 101 | tokens := sdk.TokensFromTendermintPower(power) 102 | validators[i], pool, _ = validators[i].AddTokensFromDel(pool, tokens) 103 | stakingKeeper.SetPool(ctx, pool) 104 | stakingKeeperLib.TestingUpdateValidator(stakingKeeper, ctx, validators[i], true) 105 | } 106 | 107 | return ctx, accountKeeper, keeper, bankKeeper, valAddresses, keeperErr 108 | } 109 | 110 | // nolint: unparam 111 | func CreateTestAddrs(numAddrs int) ([]sdk.AccAddress, []sdk.ValAddress) { 112 | var addresses []sdk.AccAddress 113 | var valAddresses []sdk.ValAddress 114 | var buffer bytes.Buffer 115 | 116 | // start at 100 so we can make up to 999 test addresses with valid test addresses 117 | for i := 100; i < (numAddrs + 100); i++ { 118 | numString := strconv.Itoa(i) 119 | buffer.WriteString("A58856F0FD53BF058B4909A21AEC019107BA6") //base address string 120 | 121 | buffer.WriteString(numString) //adding on final two digits to make addresses unique 122 | res, _ := sdk.AccAddressFromHex(buffer.String()) 123 | bech := res.String() 124 | address := stakingKeeperLib.TestAddr(buffer.String(), bech) 125 | valAddress := sdk.ValAddress(address) 126 | addresses = append(addresses, address) 127 | valAddresses = append(valAddresses, valAddress) 128 | buffer.Reset() 129 | } 130 | return addresses, valAddresses 131 | } 132 | 133 | // nolint: unparam 134 | func createTestPubKeys(numPubKeys int) []crypto.PubKey { 135 | var publicKeys []crypto.PubKey 136 | var buffer bytes.Buffer 137 | 138 | //start at 10 to avoid changing 1 to 01, 2 to 02, etc 139 | for i := 100; i < (numPubKeys + 100); i++ { 140 | numString := strconv.Itoa(i) 141 | buffer.WriteString("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AF") //base pubkey string 142 | buffer.WriteString(numString) //adding on final two digits to make pubkeys unique 143 | publicKeys = append(publicKeys, stakingKeeperLib.NewPubKey(buffer.String())) 144 | buffer.Reset() 145 | } 146 | return publicKeys 147 | } 148 | 149 | // MakeTestCodec creates a codec used only for testing 150 | func MakeTestCodec() *codec.Codec { 151 | var cdc = codec.New() 152 | // Register Msgs 153 | auth.RegisterCodec(cdc) 154 | bank.RegisterCodec(cdc) 155 | sdk.RegisterCodec(cdc) 156 | staking.RegisterCodec(cdc) 157 | codec.RegisterCrypto(cdc) 158 | return cdc 159 | } 160 | -------------------------------------------------------------------------------- /ethereum-contracts/README.md: -------------------------------------------------------------------------------- 1 | # Unidirectional Peggy Project Specification 2 | This project specification focuses on the role of 'Peggy', a Smart Contract system deployed to the Ethereum network as part of the Ethereum Cosmos Bridge project, and is meant to contextualize its role within the bridge. Specifications detailing structure and process of the non-Ethereum components (Relayer service, EthOracle module, Oracle module) will soon be available in the cosmos-ethereum-bridge repository linked below. 3 | 4 | ## Project Summary 5 | Unidirectional Peggy is the starting point for cross chain value transfers from the Ethereum blockchain to Cosmos based blockchains as part of the Ethereum Cosmos Bridge project. The smart contract system accepts incoming transfers of Ethereum and ERC20 tokens, locking them while the transaction is validated and equitable funds issued to the intended recipient on the Cosmos bridge chain. 6 | 7 | ## Project Background 8 | We are hoping to create a closed system for intra network transfers of cryptocurrency between blockchains, spearheaded by a proof-of-concept which enables secured transactions between Ethereum and Cosmos. 9 | 10 | ## Smart Contract Scope 11 | 12 | ### Goals of the Smart Contracts 13 | 1. Securely implement core functionality of the system such as asset locking and event emission without endangering any user funds. As such, this prototype does not permanently lock value and allows the original sender full access to their funds at any time. 14 | 2. Interface with the Relayer service, which is used by validators to listen for contract events which are signed and submitted to the Cosmos network as proof of transaction. 15 | 3. Successfully end-to-end test the Cosmos Ethereum Bridge, sending Ethereum and ERC20 tokens from Ethereum to Cosmos. 16 | 17 | ### Non-Goals of the Smart Contracts 18 | 1. Creating a production-grade system for cross-chain value transfers which enforces strict permissions and limits access to locked funds. 19 | 2. Implementing a validator set which enables observers to submit proof of fund locking transactions on Cosmos to Peggy. These features are not required for unidirectional transfers from Ethereum to Cosmos and will be re-integrated during phase two of the project, which aims to send funds from Cosmos back to Ethereum. 20 | 3. Fully gas optimize and streamline operational functionality; ease and clarity of testing has been favored over some gas management and architectural best practices. 21 | 22 | ## Ethereum Cosmos Bridge Architecture 23 | Unidirectional Peggy focuses on core features for unidirectional transfers. This prototype includes functionality to safely lock and unlock Ethereum and ERC20 tokens, emitting associated events which are witnessed by validators using the Relayer service. The Relayer is a service which interfaces with both blockchains, allowing validators to attest on the Cosmos blockchain that specific events on the Ethereum blockchain have occurred. The Relayer listens for `LogLock` events, parses information associated with the Ethereum transaction, uses it to build unsigned Cosmos transactions, and enables validators to sign and send the transactions to the Oracle module on Cosmos. Through the Relayer service, validators witness the events and submit proof in the form of signed hashes to the Cosmos based modules, which are responsible for aggregating and tallying the Validators’ signatures and their respective signing power. The system is managed by the contract's deployer, designated internally as the relayer, a trusted third-party which can unlock funds and return them their original sender. If the contract’s balances under threat, the relayer can pause the system, temporarily preventing users from depositing additional funds. 24 | 25 | The Peggy Smart Contract is deployed on the Ropsten testnet at address: 0x3de4ef81Ba6243A60B0a32d3BCeD4173b6EA02bb 26 | 27 | ### Architecture Diagram 28 | ![peggyarchitecturediagram](https://user-images.githubusercontent.com/15370712/58388886-632c7700-7fd9-11e9-962e-4e5e9d92c275.png) 29 | 30 | ### System Process: 31 | 1. Users lock Ethereum or ERC20 tokens on the Peggy contract, resulting in the emission of an event containing the created item's original sender's Ethereum address, the intended recipient's Cosmos address, the type of token, the amount locked, and the item's unique nonce. 32 | 2. Validators on the Cosmos chain witness these lock events via a Relayer service and sign a hash containing the unique item's information, which is sent as a Cosmos transaction to Oracle module. 33 | 3. Once the Oracle module has verified that the validators' aggregated signing power is greater than the specified threshold, it mints the appropriate amount of tokens and forwards them to the intended recipient. 34 | 35 | The Relayer service and Oracle module are under development here: https://github.com/swishlabsco/cosmos-ethereum-bridge. 36 | 37 | ## Installation 38 | Install Truffle: `$ npm install -g truffle` 39 | Install dependencies: `$ npm install` 40 | 41 | 42 | Note: This project currently uses solc@0.5.0, make sure that this version of the Solidity compiler is being used to compile the contracts and does not conflict with other versions that may be installed on your machine. 43 | 44 | ## Testing 45 | Run commands from the appropriate directory: `$ cd ethereum-contracts` 46 | Start the truffle environment: `$ truffle develop` 47 | In another tab, run tests: `$ truffle test` 48 | Run individual tests: `$ truffle test test/` 49 | 50 | Expected output of the test suite: 51 | ![peggytestsuite](https://user-images.githubusercontent.com/15370712/58388940-34fb6700-7fda-11e9-9aef-6ae7b2442a55.png) 52 | 53 | ## Security, Privacy, Risks 54 | Disclaimer: These contracts are for testing purposes only and are NOT intended for production. In order to prevent any loss of user funds, locked Ethereum and ERC20 tokens can be withdrawn directly by the original sender at any time. However, these contracts have not undergone external audits and should not be trusted with mainnet funds. Any use of Peggy is at the user’s own risk. 55 | 56 | ## Other Considerations 57 | We decided to temporarily remove the validator set from this version of the Smart Contracts, our reasoning being that system transparency and clarity should take precedence over the inclusion of future project features. The validator set is not required on Ethereum for unidirectional transfers and will be reimplemented once it is needed in the bidrectional version to validate transactions that have occured on Cosmos. 58 | 59 | ## Ongoing work 60 | The Ethereum Oracle module and Oracle modules are completed, with the Relayer service currently being actively integrated in order to interface between the smart contracts and Oracles. Once Ethereum -> Cosmos transfers have been successfully prototyped, functionality for bidirectional transfers (such as validator sets, signature validation, and secured token unlocking procedures) will be integrated into the contracts. Previous work in these areas is a valuable resource that will be leveraged once the complete system is ready for bidirectional transfers. 61 | 62 | Thanks to @adrianbrink, @mossid, and @sunnya97 for contributions to the original Peggy repository. 63 | -------------------------------------------------------------------------------- /cmd/ebrelayer/contract/PeggyABI.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": false, 4 | "inputs": [], 5 | "name": "activateLocking", 6 | "outputs": [], 7 | "payable": false, 8 | "stateMutability": "nonpayable", 9 | "type": "function" 10 | }, 11 | { 12 | "constant": false, 13 | "inputs": [ 14 | { 15 | "name": "_recipient", 16 | "type": "bytes" 17 | }, 18 | { 19 | "name": "_token", 20 | "type": "address" 21 | }, 22 | { 23 | "name": "_amount", 24 | "type": "uint256" 25 | } 26 | ], 27 | "name": "lock", 28 | "outputs": [ 29 | { 30 | "name": "_id", 31 | "type": "bytes32" 32 | } 33 | ], 34 | "payable": true, 35 | "stateMutability": "payable", 36 | "type": "function" 37 | }, 38 | { 39 | "constant": false, 40 | "inputs": [], 41 | "name": "pauseLocking", 42 | "outputs": [], 43 | "payable": false, 44 | "stateMutability": "nonpayable", 45 | "type": "function" 46 | }, 47 | { 48 | "constant": false, 49 | "inputs": [ 50 | { 51 | "name": "_id", 52 | "type": "bytes32" 53 | } 54 | ], 55 | "name": "unlock", 56 | "outputs": [ 57 | { 58 | "name": "", 59 | "type": "bool" 60 | } 61 | ], 62 | "payable": false, 63 | "stateMutability": "nonpayable", 64 | "type": "function" 65 | }, 66 | { 67 | "constant": false, 68 | "inputs": [ 69 | { 70 | "name": "_id", 71 | "type": "bytes32" 72 | } 73 | ], 74 | "name": "withdraw", 75 | "outputs": [ 76 | { 77 | "name": "", 78 | "type": "bool" 79 | } 80 | ], 81 | "payable": false, 82 | "stateMutability": "nonpayable", 83 | "type": "function" 84 | }, 85 | { 86 | "inputs": [], 87 | "payable": false, 88 | "stateMutability": "nonpayable", 89 | "type": "constructor" 90 | }, 91 | { 92 | "anonymous": false, 93 | "inputs": [ 94 | { 95 | "indexed": false, 96 | "name": "_id", 97 | "type": "bytes32" 98 | }, 99 | { 100 | "indexed": false, 101 | "name": "_from", 102 | "type": "address" 103 | }, 104 | { 105 | "indexed": false, 106 | "name": "_to", 107 | "type": "bytes" 108 | }, 109 | { 110 | "indexed": false, 111 | "name": "_token", 112 | "type": "address" 113 | }, 114 | { 115 | "indexed": false, 116 | "name": "_value", 117 | "type": "uint256" 118 | }, 119 | { 120 | "indexed": false, 121 | "name": "_nonce", 122 | "type": "uint256" 123 | } 124 | ], 125 | "name": "LogLock", 126 | "type": "event" 127 | }, 128 | { 129 | "anonymous": false, 130 | "inputs": [ 131 | { 132 | "indexed": false, 133 | "name": "_id", 134 | "type": "bytes32" 135 | }, 136 | { 137 | "indexed": false, 138 | "name": "_to", 139 | "type": "address" 140 | }, 141 | { 142 | "indexed": false, 143 | "name": "_token", 144 | "type": "address" 145 | }, 146 | { 147 | "indexed": false, 148 | "name": "_value", 149 | "type": "uint256" 150 | }, 151 | { 152 | "indexed": false, 153 | "name": "_nonce", 154 | "type": "uint256" 155 | } 156 | ], 157 | "name": "LogUnlock", 158 | "type": "event" 159 | }, 160 | { 161 | "anonymous": false, 162 | "inputs": [ 163 | { 164 | "indexed": false, 165 | "name": "_id", 166 | "type": "bytes32" 167 | }, 168 | { 169 | "indexed": false, 170 | "name": "_to", 171 | "type": "address" 172 | }, 173 | { 174 | "indexed": false, 175 | "name": "_token", 176 | "type": "address" 177 | }, 178 | { 179 | "indexed": false, 180 | "name": "_value", 181 | "type": "uint256" 182 | }, 183 | { 184 | "indexed": false, 185 | "name": "_nonce", 186 | "type": "uint256" 187 | } 188 | ], 189 | "name": "LogWithdraw", 190 | "type": "event" 191 | }, 192 | { 193 | "anonymous": false, 194 | "inputs": [ 195 | { 196 | "indexed": false, 197 | "name": "_time", 198 | "type": "uint256" 199 | } 200 | ], 201 | "name": "LogLockingPaused", 202 | "type": "event" 203 | }, 204 | { 205 | "anonymous": false, 206 | "inputs": [ 207 | { 208 | "indexed": false, 209 | "name": "_time", 210 | "type": "uint256" 211 | } 212 | ], 213 | "name": "LogLockingActivated", 214 | "type": "event" 215 | }, 216 | { 217 | "constant": true, 218 | "inputs": [], 219 | "name": "active", 220 | "outputs": [ 221 | { 222 | "name": "", 223 | "type": "bool" 224 | } 225 | ], 226 | "payable": false, 227 | "stateMutability": "view", 228 | "type": "function" 229 | }, 230 | { 231 | "constant": true, 232 | "inputs": [ 233 | { 234 | "name": "_id", 235 | "type": "bytes32" 236 | } 237 | ], 238 | "name": "getStatus", 239 | "outputs": [ 240 | { 241 | "name": "", 242 | "type": "bool" 243 | } 244 | ], 245 | "payable": false, 246 | "stateMutability": "view", 247 | "type": "function" 248 | }, 249 | { 250 | "constant": true, 251 | "inputs": [ 252 | { 253 | "name": "", 254 | "type": "bytes32" 255 | } 256 | ], 257 | "name": "ids", 258 | "outputs": [ 259 | { 260 | "name": "", 261 | "type": "bool" 262 | } 263 | ], 264 | "payable": false, 265 | "stateMutability": "view", 266 | "type": "function" 267 | }, 268 | { 269 | "constant": true, 270 | "inputs": [], 271 | "name": "nonce", 272 | "outputs": [ 273 | { 274 | "name": "", 275 | "type": "uint256" 276 | } 277 | ], 278 | "payable": false, 279 | "stateMutability": "view", 280 | "type": "function" 281 | }, 282 | { 283 | "constant": true, 284 | "inputs": [], 285 | "name": "relayer", 286 | "outputs": [ 287 | { 288 | "name": "", 289 | "type": "address" 290 | } 291 | ], 292 | "payable": false, 293 | "stateMutability": "view", 294 | "type": "function" 295 | }, 296 | { 297 | "constant": true, 298 | "inputs": [ 299 | { 300 | "name": "_id", 301 | "type": "bytes32" 302 | } 303 | ], 304 | "name": "viewItem", 305 | "outputs": [ 306 | { 307 | "name": "", 308 | "type": "address" 309 | }, 310 | { 311 | "name": "", 312 | "type": "bytes" 313 | }, 314 | { 315 | "name": "", 316 | "type": "address" 317 | }, 318 | { 319 | "name": "", 320 | "type": "uint256" 321 | }, 322 | { 323 | "name": "", 324 | "type": "uint256" 325 | } 326 | ], 327 | "payable": false, 328 | "stateMutability": "view", 329 | "type": "function" 330 | } 331 | ] -------------------------------------------------------------------------------- /ethereum-contracts/contracts/Peggy.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.5.0; 2 | 3 | import "./Processor.sol"; 4 | 5 | /* 6 | * @title: Peggy 7 | * @dev: Peg zone contract for testing one-way transfers from Ethereum 8 | * to Cosmos, facilitated by a trusted relayer. This contract is 9 | * NOT intended to be used in production and users are empowered 10 | * to withdraw their locked funds at any time. 11 | */ 12 | contract Peggy is Processor { 13 | 14 | bool public active; 15 | address public relayer; 16 | mapping(bytes32 => bool) public ids; 17 | 18 | event LogLock( 19 | bytes32 _id, 20 | address _from, 21 | bytes _to, 22 | address _token, 23 | uint256 _value, 24 | uint256 _nonce 25 | ); 26 | 27 | event LogUnlock( 28 | bytes32 _id, 29 | address _to, 30 | address _token, 31 | uint256 _value, 32 | uint256 _nonce 33 | ); 34 | 35 | event LogWithdraw( 36 | bytes32 _id, 37 | address _to, 38 | address _token, 39 | uint256 _value, 40 | uint256 _nonce 41 | ); 42 | 43 | event LogLockingPaused( 44 | uint256 _time 45 | ); 46 | 47 | event LogLockingActivated( 48 | uint256 _time 49 | ); 50 | 51 | /* 52 | * @dev: Modifier to restrict access to the relayer. 53 | * 54 | */ 55 | modifier onlyRelayer() 56 | { 57 | require( 58 | msg.sender == relayer, 59 | 'Must be the specified relayer.' 60 | ); 61 | _; 62 | } 63 | 64 | /* 65 | * @dev: Modifier which restricts lock functionality when paused. 66 | * 67 | */ 68 | modifier whileActive() 69 | { 70 | require( 71 | active == true, 72 | 'Lock functionality is currently paused.' 73 | ); 74 | _; 75 | } 76 | /* 77 | * @dev: Constructor, initalizes relayer and active status. 78 | * 79 | */ 80 | constructor() 81 | public 82 | { 83 | relayer = msg.sender; 84 | active = true; 85 | emit LogLockingActivated(now); 86 | } 87 | 88 | /* 89 | * @dev: Locks received funds and creates new items. 90 | * 91 | * @param _recipient: bytes representation of destination address. 92 | * @param _token: token address in origin chain (0x0 if ethereum) 93 | * @param _amount: value of item 94 | */ 95 | function lock( 96 | bytes memory _recipient, 97 | address _token, 98 | uint256 _amount 99 | ) 100 | public 101 | payable 102 | availableNonce() 103 | whileActive() 104 | returns(bytes32 _id) 105 | { 106 | //Actions based on token address type 107 | if (msg.value != 0) { 108 | require(_token == address(0)); 109 | require(msg.value == _amount); 110 | } else { 111 | require(ERC20(_token).transferFrom(msg.sender, address(this), _amount)); 112 | } 113 | 114 | //Create an item with a unique key. 115 | bytes32 id = create( 116 | msg.sender, 117 | _recipient, 118 | _token, 119 | _amount 120 | ); 121 | 122 | emit LogLock( 123 | id, 124 | msg.sender, 125 | _recipient, 126 | _token, 127 | _amount, 128 | getNonce() 129 | ); 130 | 131 | return id; 132 | } 133 | 134 | /* 135 | * @dev: Unlocks ethereum/erc20 tokens, called by relayer. 136 | * 137 | * This is a shortcut utility method for testing purposes. 138 | * In the future bidirectional system, unlocking functionality 139 | * will be guarded by validator signatures. 140 | * 141 | * @param _id: Unique key of the item. 142 | */ 143 | function unlock( 144 | bytes32 _id 145 | ) 146 | onlyRelayer 147 | canDeliver(_id) 148 | external 149 | returns (bool) 150 | { 151 | require(isLocked(_id)); 152 | 153 | // Transfer item's funds and unlock it 154 | (address payable sender, 155 | address token, 156 | uint256 amount, 157 | uint256 uniqueNonce) = complete(_id); 158 | 159 | //Emit unlock event 160 | emit LogUnlock( 161 | _id, 162 | sender, 163 | token, 164 | amount, 165 | uniqueNonce 166 | ); 167 | return true; 168 | } 169 | 170 | /* 171 | * @dev: Withdraws ethereum/erc20 tokens, called original sender. 172 | * 173 | * This is a backdoor utility method included for testing, 174 | * purposes, allowing users to withdraw their funds. This 175 | * functionality will be removed in production. 176 | * 177 | * @param _id: Unique key of the item. 178 | */ 179 | function withdraw( 180 | bytes32 _id 181 | ) 182 | onlySender(_id) 183 | canDeliver(_id) 184 | external 185 | returns (bool) 186 | { 187 | require(isLocked(_id)); 188 | 189 | // Transfer item's funds and unlock it 190 | (address payable sender, 191 | address token, 192 | uint256 amount, 193 | uint256 uniqueNonce) = complete(_id); 194 | 195 | //Emit withdraw event 196 | emit LogWithdraw( 197 | _id, 198 | sender, 199 | token, 200 | amount, 201 | uniqueNonce 202 | ); 203 | 204 | return true; 205 | } 206 | 207 | /* 208 | * @dev: Exposes an item's current status. 209 | * 210 | * @param _id: The item in question. 211 | * @return: Boolean indicating the lock status. 212 | */ 213 | function getStatus( 214 | bytes32 _id 215 | ) 216 | public 217 | view 218 | returns(bool) 219 | { 220 | return isLocked(_id); 221 | } 222 | 223 | /* 224 | * @dev: Allows access to an item's information via its unique identifier. 225 | * 226 | * @param _id: The item to be viewed. 227 | * @return: Original sender's address. 228 | * @return: Intended receiver's address in bytes. 229 | * @return: The token's address. 230 | * @return: The amount locked in the item. 231 | * @return: The item's unique nonce. 232 | */ 233 | function viewItem( 234 | bytes32 _id 235 | ) 236 | public 237 | view 238 | returns(address, bytes memory, address, uint256, uint256) 239 | { 240 | return getItem(_id); 241 | } 242 | 243 | /* 244 | * @dev: Relayer can pause fund locking without impacting other functionality. 245 | */ 246 | function pauseLocking() 247 | public 248 | onlyRelayer 249 | { 250 | require(active); 251 | active = false; 252 | emit LogLockingPaused(now); 253 | } 254 | 255 | /* 256 | * @dev: Relayer can activate fund locking without impacting other functionality. 257 | */ 258 | function activateLocking() 259 | public 260 | onlyRelayer 261 | { 262 | require(!active); 263 | active = true; 264 | emit LogLockingActivated(now); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/tendermint/tendermint/libs/log" 5 | 6 | "github.com/cosmos/cosmos-sdk/codec" 7 | "github.com/cosmos/cosmos-sdk/x/auth" 8 | "github.com/cosmos/cosmos-sdk/x/bank" 9 | "github.com/cosmos/cosmos-sdk/x/params" 10 | "github.com/cosmos/cosmos-sdk/x/staking" 11 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/ethbridge" 12 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle" 13 | 14 | bam "github.com/cosmos/cosmos-sdk/baseapp" 15 | sdk "github.com/cosmos/cosmos-sdk/types" 16 | abci "github.com/tendermint/tendermint/abci/types" 17 | cmn "github.com/tendermint/tendermint/libs/common" 18 | dbm "github.com/tendermint/tendermint/libs/db" 19 | ) 20 | 21 | const ( 22 | appName = "ethereum-bridge" 23 | ) 24 | 25 | type ethereumBridgeApp struct { 26 | *bam.BaseApp 27 | cdc *codec.Codec 28 | 29 | keyMain *sdk.KVStoreKey 30 | keyAccount *sdk.KVStoreKey 31 | keyStaking *sdk.KVStoreKey 32 | tkeyStaking *sdk.TransientStoreKey 33 | keyOracle *sdk.KVStoreKey 34 | keyFeeCollection *sdk.KVStoreKey 35 | keyParams *sdk.KVStoreKey 36 | tkeyParams *sdk.TransientStoreKey 37 | 38 | accountKeeper auth.AccountKeeper 39 | bankKeeper bank.Keeper 40 | feeCollectionKeeper auth.FeeCollectionKeeper 41 | stakingKeeper staking.Keeper 42 | 43 | paramsKeeper params.Keeper 44 | oracleKeeper oracle.Keeper 45 | } 46 | 47 | // NewEthereumBridgeApp is a constructor function for ethereumBridgeApp 48 | func NewEthereumBridgeApp(logger log.Logger, db dbm.DB) *ethereumBridgeApp { 49 | 50 | // First define the top level codec that will be shared by the different modules 51 | cdc := MakeCodec() 52 | 53 | // BaseApp handles interactions with Tendermint through the ABCI protocol 54 | bApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc)) 55 | 56 | // Here you initialize your application with the store keys it requires 57 | var app = ðereumBridgeApp{ 58 | BaseApp: bApp, 59 | cdc: cdc, 60 | 61 | keyMain: sdk.NewKVStoreKey(bam.MainStoreKey), 62 | keyAccount: sdk.NewKVStoreKey(auth.StoreKey), 63 | keyStaking: sdk.NewKVStoreKey(staking.StoreKey), 64 | tkeyStaking: sdk.NewTransientStoreKey(staking.TStoreKey), 65 | keyOracle: sdk.NewKVStoreKey(oracle.StoreKey), 66 | keyFeeCollection: sdk.NewKVStoreKey(auth.FeeStoreKey), 67 | keyParams: sdk.NewKVStoreKey(params.StoreKey), 68 | tkeyParams: sdk.NewTransientStoreKey(params.TStoreKey), 69 | } 70 | 71 | // The ParamsKeeper handles parameter storage for the application 72 | app.paramsKeeper = params.NewKeeper(app.cdc, app.keyParams, app.tkeyParams) 73 | 74 | // The AccountKeeper handles address -> account lookups 75 | app.accountKeeper = auth.NewAccountKeeper( 76 | app.cdc, 77 | app.keyAccount, 78 | app.paramsKeeper.Subspace(auth.DefaultParamspace), 79 | auth.ProtoBaseAccount, 80 | ) 81 | 82 | // The BankKeeper allows you perform sdk.Coins interactions 83 | app.bankKeeper = bank.NewBaseKeeper( 84 | app.accountKeeper, 85 | app.paramsKeeper.Subspace(bank.DefaultParamspace), 86 | bank.DefaultCodespace, 87 | ) 88 | 89 | // The FeeCollectionKeeper collects transaction fees and renders them to the fee distribution module 90 | app.feeCollectionKeeper = auth.NewFeeCollectionKeeper(cdc, app.keyFeeCollection) 91 | 92 | app.stakingKeeper = staking.NewKeeper( 93 | app.cdc, 94 | app.keyStaking, app.tkeyStaking, 95 | app.bankKeeper, app.paramsKeeper.Subspace(staking.DefaultParamspace), 96 | staking.DefaultCodespace, 97 | ) 98 | 99 | // The OracleKeeper is the Keeper from the oracle module 100 | // It handles interactions with the oracle store 101 | oracleKeeper, oracleErr := oracle.NewKeeper( 102 | app.stakingKeeper, 103 | app.keyOracle, 104 | app.cdc, 105 | oracle.DefaultCodespace, 106 | oracle.DefaultConsensusNeeded, 107 | ) 108 | if oracleErr != nil { 109 | cmn.Exit(oracleErr.Error()) 110 | } 111 | app.oracleKeeper = oracleKeeper 112 | 113 | // The AnteHandler handles signature verification and transaction pre-processing 114 | app.SetAnteHandler(auth.NewAnteHandler(app.accountKeeper, app.feeCollectionKeeper)) 115 | 116 | // The app.Router is the main transaction router where each module registers its routes 117 | // Register the bank route here 118 | app.Router(). 119 | AddRoute(bank.RouterKey, bank.NewHandler(app.bankKeeper)). 120 | AddRoute(staking.RouterKey, staking.NewHandler(app.stakingKeeper)). 121 | AddRoute(ethbridge.RouterKey, ethbridge.NewHandler(app.oracleKeeper, app.bankKeeper, app.cdc, ethbridge.DefaultCodespace)) 122 | 123 | // The app.QueryRouter is the main query router where each module registers its routes 124 | app.QueryRouter(). 125 | AddRoute(auth.QuerierRoute, auth.NewQuerier(app.accountKeeper)). 126 | AddRoute(staking.QuerierRoute, staking.NewQuerier(app.stakingKeeper, app.cdc)). 127 | AddRoute(ethbridge.QuerierRoute, ethbridge.NewQuerier(app.oracleKeeper, app.cdc, ethbridge.DefaultCodespace)) 128 | 129 | // The initChainer handles translating the genesis.json file into initial state for the network 130 | app.SetInitChainer(app.initChainer) 131 | 132 | app.MountStores( 133 | app.keyMain, 134 | app.keyAccount, 135 | app.keyStaking, 136 | app.keyOracle, 137 | app.keyFeeCollection, 138 | app.keyParams, 139 | app.tkeyParams, 140 | app.tkeyStaking, 141 | ) 142 | 143 | app.SetEndBlocker(app.EndBlocker) 144 | 145 | err := app.LoadLatestVersion(app.keyMain) 146 | if err != nil { 147 | cmn.Exit(err.Error()) 148 | } 149 | 150 | return app 151 | } 152 | 153 | // initialize store from a genesis state 154 | func (app *ethereumBridgeApp) initFromGenesisState(ctx sdk.Context, genesisState GenesisState) []abci.ValidatorUpdate { 155 | 156 | // // load the accounts 157 | for _, gacc := range genesisState.Accounts { 158 | acc := gacc.ToAccount() 159 | acc = app.accountKeeper.NewAccount(ctx, acc) // set account number 160 | app.accountKeeper.SetAccount(ctx, acc) 161 | } 162 | 163 | // load the initial staking information 164 | validators, err := staking.InitGenesis(ctx, app.stakingKeeper, genesisState.StakingData) 165 | if err != nil { 166 | panic(err) 167 | } 168 | 169 | // initialize module-specific stores 170 | auth.InitGenesis(ctx, app.accountKeeper, app.feeCollectionKeeper, genesisState.AuthData) 171 | bank.InitGenesis(ctx, app.bankKeeper, genesisState.BankData) 172 | 173 | if len(genesisState.GenTxs) > 0 { 174 | for _, genTx := range genesisState.GenTxs { 175 | var tx auth.StdTx 176 | err = app.cdc.UnmarshalJSON(genTx, &tx) 177 | if err != nil { 178 | panic(err) 179 | } 180 | bz := app.cdc.MustMarshalBinaryLengthPrefixed(tx) 181 | res := app.BaseApp.DeliverTx(bz) 182 | if !res.IsOK() { 183 | panic(res.Log) 184 | } 185 | } 186 | validators = app.stakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx) 187 | 188 | } 189 | 190 | return validators 191 | } 192 | 193 | func (app *ethereumBridgeApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { 194 | stateJSON := req.AppStateBytes 195 | 196 | genesisState := new(GenesisState) 197 | err := app.cdc.UnmarshalJSON(stateJSON, genesisState) 198 | if err != nil { 199 | panic(err) 200 | } 201 | 202 | validators := app.initFromGenesisState(ctx, *genesisState) 203 | 204 | return abci.ResponseInitChain{ 205 | Validators: validators, 206 | } 207 | } 208 | 209 | // MakeCodec generates the necessary codecs for Amino 210 | func MakeCodec() *codec.Codec { 211 | var cdc = codec.New() 212 | auth.RegisterCodec(cdc) 213 | bank.RegisterCodec(cdc) 214 | staking.RegisterCodec(cdc) 215 | ethbridge.RegisterCodec(cdc) 216 | sdk.RegisterCodec(cdc) 217 | codec.RegisterCrypto(cdc) 218 | return cdc 219 | } 220 | 221 | // application updates every end block 222 | // nolint: unparam 223 | func (app *ethereumBridgeApp) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { 224 | validatorUpdates, _ := staking.EndBlocker(ctx, app.stakingKeeper) 225 | 226 | return abci.ResponseEndBlock{ 227 | ValidatorUpdates: validatorUpdates, 228 | } 229 | } 230 | 231 | // load a particular height 232 | func (app *ethereumBridgeApp) LoadHeight(height int64) error { 233 | return app.LoadVersion(height, app.keyMain) 234 | } 235 | -------------------------------------------------------------------------------- /ethereum-contracts/test/test_processor.js: -------------------------------------------------------------------------------- 1 | const TestProcessor = artifacts.require('TestProcessor'); 2 | const TestToken = artifacts.require('TestToken'); 3 | 4 | const Web3Utils = require('web3-utils'); 5 | const EVMRevert = 'revert'; 6 | const BigNumber = web3.BigNumber; 7 | 8 | require('chai') 9 | .use(require('chai-as-promised')) 10 | .use(require('chai-bignumber')(BigNumber)) 11 | .should(); 12 | 13 | contract('TestProcessor', function (accounts) { 14 | 15 | const userOne = accounts[1]; 16 | const userTwo = accounts[2]; 17 | const userThree = accounts[3]; 18 | 19 | describe('Processor contract deployment', function() { 20 | 21 | beforeEach(async function() { 22 | this.processor = await TestProcessor.new(); 23 | }); 24 | 25 | it('should deploy the processor with the correct parameters', async function () { 26 | this.processor.should.exist; 27 | 28 | const nonce = Number(await this.processor.nonce()); 29 | nonce.should.be.bignumber.equal(0); 30 | }); 31 | 32 | }); 33 | 34 | describe('Item creation', function() { 35 | 36 | beforeEach(async function() { 37 | this.processor = await TestProcessor.new(); 38 | this.recipient = web3.utils.bytesToHex(['20bytestring']) 39 | this.amount = 250; 40 | 41 | //Load user account with tokens for testing 42 | this.token = await TestToken.new(); 43 | await this.token.mint(userOne, 1000, { from: accounts[0] }).should.be.fulfilled; 44 | }); 45 | 46 | 47 | it('should allow for the creation of items', async function () { 48 | await this.processor.callCreate(userOne, this.recipient, this.token.address, this.amount).should.be.fulfilled; 49 | }); 50 | 51 | it('should generate unique item id\'s for a created item', async function () { 52 | //Simulate sha3 hash to get item's expected id 53 | const expectedId = Web3Utils.soliditySha3( 54 | {t: 'address payable', v: userOne}, 55 | {t: 'bytes', v: this.recipient}, 56 | {t: 'address', v: this.token.address}, 57 | {t: 'int256', v:this.amount}, 58 | {t: 'int256', v:1}); 59 | 60 | //Get the item's id if it were to be created 61 | const id = await this.processor.callCreate.call(userOne, this.recipient, this.token.address, this.amount); 62 | id.should.be.equal(expectedId); 63 | }); 64 | 65 | it('should allow access to an item\'s information given it\'s unique id', async function () { 66 | const id = await this.processor.callCreate.call(userOne, this.recipient, this.token.address, this.amount); 67 | await this.processor.callCreate(userOne, this.recipient, this.token.address, this.amount); 68 | 69 | //Attempt to get an item's information 70 | await this.processor.callGetItem(id).should.be.fulfilled; 71 | }); 72 | 73 | it('should correctly identify the existence of items in memory', async function () { 74 | //Get the item's expected id then lock funds 75 | const id = await this.processor.callCreate.call(userOne, this.recipient, this.token.address, this.amount); 76 | await this.processor.callCreate(userOne, this.recipient, this.token.address, this.amount).should.be.fulfilled; 77 | 78 | //Check if item has been created and locked 79 | const locked = await this.processor.callIsLocked(id); 80 | locked.should.be.equal(true); 81 | }); 82 | 83 | it('should store items with the correct parameters', async function () { 84 | //Create the item and store its id 85 | const id = await this.processor.callCreate.call(userOne, this.recipient, this.token.address, this.amount); 86 | await this.processor.callCreate(userOne, this.recipient, this.token.address, this.amount); 87 | 88 | //Get the item's information 89 | const itemInfo = await this.processor.callGetItem(id); 90 | 91 | //Parse each attribute 92 | const sender = itemInfo[0]; 93 | const receiver = itemInfo[1]; 94 | const token = itemInfo[2]; 95 | const amount = Number(itemInfo[3]); 96 | const nonce = Number(itemInfo[4]); 97 | 98 | //Confirm that each attribute is correct 99 | sender.should.be.equal(userOne); 100 | receiver.should.be.equal(this.recipient); 101 | token.should.be.equal(this.token.address); 102 | amount.should.be.bignumber.equal(this.amount); 103 | nonce.should.be.bignumber.equal(1); 104 | }); 105 | 106 | }); 107 | 108 | describe('Item completion', function() { 109 | 110 | beforeEach(async function() { 111 | this.processor = await TestProcessor.new(); 112 | this.weiAmount = web3.utils.toWei("0.25", "ether"); 113 | this.recipient = web3.utils.bytesToHex(['20bytestring']) 114 | this.ethereumToken = '0x0000000000000000000000000000000000000000'; 115 | 116 | //Load contract with ethereum so it can complete items 117 | await this.processor.send(web3.utils.toWei("1", "ether"), { from: accounts[0]}).should.be.fulfilled; 118 | 119 | this.itemId = await this.processor.callCreate.call(userOne, this.recipient, this.ethereumToken, this.weiAmount); 120 | await this.processor.callCreate(userOne, this.recipient, this.ethereumToken, this.weiAmount); 121 | }); 122 | 123 | it('should not allow for the completion of items whose value exceeds the contract\'s balance', async function () { 124 | //Create an item with an overlimit amount 125 | const overlimitAmount = web3.utils.toWei("1.25", "ether"); 126 | const id = await this.processor.callCreate.call(userOne, this.recipient, this.ethereumToken, overlimitAmount); 127 | await this.processor.callCreate(userOne, this.recipient, this.ethereumToken, overlimitAmount); 128 | 129 | //Attempt to complete the item 130 | await this.processor.callComplete(id).should.be.rejectedWith(EVMRevert); 131 | }); 132 | 133 | it('should not allow for the completion of non-items', async function () { 134 | //Generate a false item id 135 | const fakeId = Web3Utils.soliditySha3( 136 | {t: 'address payable', v: userOne}, 137 | {t: 'bytes', v: this.recipient}, 138 | {t: 'address', v: this.ethereumToken}, 139 | {t: 'int256', v:12}, 140 | {t: 'int256', v:1}); 141 | 142 | await this.processor.callComplete(fakeId).should.be.rejectedWith(EVMRevert); 143 | 144 | }); 145 | 146 | it('should not allow for the completion of an item that has already been completed', async function () { 147 | //Complete the item 148 | await this.processor.callComplete(this.itemId).should.be.fulfilled; 149 | 150 | //Attempt to complete the item again 151 | await this.processor.callComplete(this.itemId).should.be.rejectedWith(EVMRevert); 152 | }); 153 | 154 | it('should allow for an item to be completed', async function () { 155 | await this.processor.callComplete(this.itemId).should.be.fulfilled; 156 | }); 157 | 158 | it('should update lock status of items upon completion', async function () { 159 | //Confirm that the item is active 160 | const startingLockStatus = await this.processor.callIsLocked(this.itemId); 161 | startingLockStatus.should.be.equal(true); 162 | 163 | //Complete the item 164 | await this.processor.callComplete(this.itemId).should.be.fulfilled; 165 | 166 | //Check if the item still exists 167 | const completedItem = await this.processor.callIsLocked(this.itemId); 168 | completedItem.should.be.equal(false); 169 | }); 170 | 171 | it('should correctly transfer itemized funds to the original sender', async function () { 172 | //Get prior balances of user and peggy contract 173 | const beforeUserBalance = Number(await web3.eth.getBalance(userOne)); 174 | const beforeContractBalance = Number(await web3.eth.getBalance(this.processor.address)); 175 | 176 | await this.processor.callComplete(this.itemId).should.be.fulfilled; 177 | 178 | //Get balances after completion 179 | const afterUserBalance = Number(await web3.eth.getBalance(userOne)); 180 | const afterContractBalance = Number(await web3.eth.getBalance(this.processor.address)); 181 | 182 | //Expected balances 183 | afterUserBalance.should.be.bignumber.equal(beforeUserBalance + Number(this.weiAmount)); 184 | afterContractBalance.should.be.bignumber.equal(beforeContractBalance - Number(this.weiAmount)); 185 | }); 186 | 187 | }); 188 | 189 | }); -------------------------------------------------------------------------------- /cmd/ebd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/cosmos/cosmos-sdk/client" 13 | "github.com/cosmos/cosmos-sdk/client/keys" 14 | "github.com/cosmos/cosmos-sdk/codec" 15 | "github.com/cosmos/cosmos-sdk/server" 16 | "github.com/cosmos/cosmos-sdk/x/auth" 17 | "github.com/cosmos/cosmos-sdk/x/bank" 18 | "github.com/cosmos/cosmos-sdk/x/staking" 19 | 20 | "github.com/spf13/cobra" 21 | "github.com/spf13/viper" 22 | "github.com/tendermint/tendermint/crypto" 23 | "github.com/tendermint/tendermint/libs/cli" 24 | "github.com/tendermint/tendermint/libs/common" 25 | "github.com/tendermint/tendermint/libs/log" 26 | "github.com/tendermint/tendermint/types" 27 | 28 | app "github.com/swishlabsco/cosmos-ethereum-bridge" 29 | 30 | gaiaInit "github.com/cosmos/cosmos-sdk/cmd/gaia/init" 31 | sdk "github.com/cosmos/cosmos-sdk/types" 32 | abci "github.com/tendermint/tendermint/abci/types" 33 | cfg "github.com/tendermint/tendermint/config" 34 | dbm "github.com/tendermint/tendermint/libs/db" 35 | tmtypes "github.com/tendermint/tendermint/types" 36 | ) 37 | 38 | // DefaultNodeHome sets the folder where the applcation data and configuration will be stored 39 | var DefaultNodeHome = os.ExpandEnv("$HOME/.ebd") 40 | 41 | const ( 42 | flagOverwrite = "overwrite" 43 | flagClientHome = "home-client" 44 | flagVestingStart = "vesting-start-time" 45 | flagVestingEnd = "vesting-end-time" 46 | flagVestingAmt = "vesting-amount" 47 | ) 48 | 49 | func main() { 50 | cobra.EnableCommandSorting = false 51 | 52 | cdc := app.MakeCodec() 53 | ctx := server.NewDefaultContext() 54 | 55 | rootCmd := &cobra.Command{ 56 | Use: "ebd", 57 | Short: "ethereum bridge App Daemon (server)", 58 | PersistentPreRunE: server.PersistentPreRunEFn(ctx), 59 | } 60 | 61 | rootCmd.AddCommand(InitCmd(ctx, cdc)) 62 | rootCmd.AddCommand(AddGenesisAccountCmd(ctx, cdc)) 63 | server.AddCommands(ctx, cdc, rootCmd, newApp, exportAppStateAndTMValidators) 64 | 65 | // prepare and add flags 66 | executor := cli.PrepareBaseCmd(rootCmd, "EB", DefaultNodeHome) 67 | err := executor.Execute() 68 | if err != nil { 69 | // handle with #870 70 | panic(err) 71 | } 72 | } 73 | 74 | func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer) abci.Application { 75 | return app.NewEthereumBridgeApp(logger, db) 76 | } 77 | 78 | func exportAppStateAndTMValidators( 79 | logger log.Logger, db dbm.DB, traceStore io.Writer, height int64, forZeroHeight bool, jailWhiteList []string, 80 | ) (json.RawMessage, []tmtypes.GenesisValidator, error) { 81 | 82 | if height != -1 { 83 | ebApp := app.NewEthereumBridgeApp(logger, db) 84 | err := ebApp.LoadHeight(height) 85 | if err != nil { 86 | return nil, nil, err 87 | } 88 | return ebApp.ExportAppStateAndValidators(forZeroHeight, jailWhiteList) 89 | } 90 | ebApp := app.NewEthereumBridgeApp(logger, db) 91 | return ebApp.ExportAppStateAndValidators(forZeroHeight, jailWhiteList) 92 | } 93 | 94 | // InitCmd initializes all files for tendermint and application 95 | func InitCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command { 96 | cmd := &cobra.Command{ 97 | Use: "init", 98 | Short: "Initialize genesis config, priv-validator file, and p2p-node file", 99 | Args: cobra.NoArgs, 100 | RunE: func(_ *cobra.Command, _ []string) error { 101 | config := ctx.Config 102 | config.SetRoot(viper.GetString(cli.HomeFlag)) 103 | 104 | chainID := viper.GetString(client.FlagChainID) 105 | if chainID == "" { 106 | chainID = fmt.Sprintf("test-chain-%v", common.RandStr(6)) 107 | } 108 | 109 | _, pk, err := gaiaInit.InitializeNodeValidatorFiles(config) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | var appState json.RawMessage 115 | genFile := config.GenesisFile() 116 | 117 | if !viper.GetBool(flagOverwrite) && common.FileExists(genFile) { 118 | return fmt.Errorf("genesis.json file already exists: %v", genFile) 119 | } 120 | 121 | genesis := app.GenesisState{ 122 | AuthData: auth.DefaultGenesisState(), 123 | BankData: bank.DefaultGenesisState(), 124 | StakingData: staking.DefaultGenesisState(), 125 | } 126 | 127 | appState, err = codec.MarshalJSONIndent(cdc, genesis) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | _, _, validator, err := SimpleAppGenTx(cdc, pk) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | if err = gaiaInit.ExportGenesisFile(genFile, chainID, []tmtypes.GenesisValidator{validator}, appState); err != nil { 138 | return err 139 | } 140 | 141 | cfg.WriteConfigFile(filepath.Join(config.RootDir, "config", "config.toml"), config) 142 | 143 | fmt.Printf("Initialized ebd configuration and bootstrapping files in %s...\n", viper.GetString(cli.HomeFlag)) 144 | return nil 145 | }, 146 | } 147 | 148 | cmd.Flags().String(cli.HomeFlag, DefaultNodeHome, "node's home directory") 149 | cmd.Flags().String(client.FlagChainID, "", "genesis file chain-id, if left blank will be randomly created") 150 | cmd.Flags().BoolP(flagOverwrite, "o", false, "overwrite the genesis.json file") 151 | 152 | return cmd 153 | } 154 | 155 | // AddGenesisAccountCmd allows users to add accounts to the genesis file 156 | func AddGenesisAccountCmd(ctx *server.Context, cdc *codec.Codec) *cobra.Command { 157 | cmd := &cobra.Command{ 158 | Use: "add-genesis-account [address] [coins[,coins]]", 159 | Short: "Adds an account to the genesis file", 160 | Args: cobra.ExactArgs(2), 161 | Long: strings.TrimSpace(` 162 | Adds accounts to the genesis file so that you can start a chain with coins in the CLI: 163 | 164 | $ ebd add-genesis-account cosmos1tse7r2fadvlrrgau3pa0ss7cqh55wrv6y9alwh 1000STAKE,1000nametoken 165 | `), 166 | RunE: func(_ *cobra.Command, args []string) error { 167 | config := ctx.Config 168 | config.SetRoot(viper.GetString(cli.HomeFlag)) 169 | 170 | addr, err := sdk.AccAddressFromBech32(args[0]) 171 | if err != nil { 172 | kb, err := keys.NewKeyBaseFromDir(viper.GetString(flagClientHome)) 173 | if err != nil { 174 | return err 175 | } 176 | 177 | info, err := kb.Get(args[0]) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | addr = info.GetAddress() 183 | } 184 | 185 | coins, err := sdk.ParseCoins(args[1]) 186 | if err != nil { 187 | return err 188 | } 189 | 190 | vestingStart := viper.GetInt64(flagVestingStart) 191 | vestingEnd := viper.GetInt64(flagVestingEnd) 192 | vestingAmt, err := sdk.ParseCoins(viper.GetString(flagVestingAmt)) 193 | if err != nil { 194 | return err 195 | } 196 | 197 | genFile := config.GenesisFile() 198 | if !common.FileExists(genFile) { 199 | return fmt.Errorf("%s does not exist, run `gaiad init` first", genFile) 200 | } 201 | 202 | genContents, err := ioutil.ReadFile(genFile) 203 | if err != nil { 204 | return err 205 | } 206 | 207 | var genDoc types.GenesisDoc 208 | if err := cdc.UnmarshalJSON(genContents, &genDoc); err != nil { 209 | return err 210 | } 211 | 212 | var appState app.GenesisState 213 | if err = cdc.UnmarshalJSON(genDoc.AppState, &appState); err != nil { 214 | return err 215 | } 216 | 217 | appState, err = addGenesisAccount(cdc, appState, addr, coins, vestingAmt, vestingStart, vestingEnd) 218 | if err != nil { 219 | return err 220 | } 221 | 222 | appStateJSON, err := cdc.MarshalJSON(appState) 223 | if err != nil { 224 | return err 225 | } 226 | 227 | return gaiaInit.ExportGenesisFile(genFile, genDoc.ChainID, genDoc.Validators, appStateJSON) 228 | }, 229 | } 230 | cmd.Flags().String(flagVestingAmt, "", "amount of coins for vesting accounts") 231 | cmd.Flags().Uint64(flagVestingStart, 0, "schedule start time (unix epoch) for vesting accounts") 232 | cmd.Flags().Uint64(flagVestingEnd, 0, "schedule end time (unix epoch) for vesting accounts") 233 | return cmd 234 | } 235 | 236 | // SimpleAppGenTx returns a simple GenTx command that makes the node a valdiator from the start 237 | func SimpleAppGenTx(cdc *codec.Codec, pk crypto.PubKey) ( 238 | appGenTx, cliPrint json.RawMessage, validator tmtypes.GenesisValidator, err error) { 239 | 240 | addr, secret, err := server.GenerateCoinKey() 241 | if err != nil { 242 | return 243 | } 244 | 245 | bz, err := cdc.MarshalJSON(struct { 246 | Addr sdk.AccAddress `json:"addr"` 247 | }{addr}) 248 | if err != nil { 249 | return 250 | } 251 | 252 | appGenTx = json.RawMessage(bz) 253 | 254 | bz, err = cdc.MarshalJSON(map[string]string{"secret": secret}) 255 | if err != nil { 256 | return 257 | } 258 | 259 | cliPrint = json.RawMessage(bz) 260 | 261 | validator = tmtypes.GenesisValidator{ 262 | PubKey: pk, 263 | Power: 10, 264 | } 265 | 266 | return 267 | } 268 | 269 | func addGenesisAccount( 270 | cdc *codec.Codec, appState app.GenesisState, addr sdk.AccAddress, 271 | coins, vestingAmt sdk.Coins, vestingStart, vestingEnd int64, 272 | ) (app.GenesisState, error) { 273 | 274 | for _, stateAcc := range appState.Accounts { 275 | if stateAcc.Address.Equals(addr) { 276 | return appState, fmt.Errorf("the application state already contains account %v", addr) 277 | } 278 | } 279 | 280 | acc := auth.NewBaseAccountWithAddress(addr) 281 | acc.Coins = coins 282 | appState.StakingData.Pool.NotBondedTokens = appState.StakingData.Pool.NotBondedTokens.Add(coins.AmountOf(appState.StakingData.Params.BondDenom)) 283 | 284 | if !vestingAmt.IsZero() { 285 | var vacc auth.VestingAccount 286 | 287 | bvacc := &auth.BaseVestingAccount{ 288 | BaseAccount: &acc, 289 | OriginalVesting: vestingAmt, 290 | EndTime: vestingEnd, 291 | } 292 | 293 | if bvacc.OriginalVesting.IsAllGT(acc.Coins) { 294 | return appState, fmt.Errorf("vesting amount cannot be greater than total amount") 295 | } 296 | if vestingStart >= vestingEnd { 297 | return appState, fmt.Errorf("vesting start time must before end time") 298 | } 299 | 300 | if vestingStart != 0 { 301 | vacc = &auth.ContinuousVestingAccount{ 302 | BaseVestingAccount: bvacc, 303 | StartTime: vestingStart, 304 | } 305 | } else { 306 | vacc = &auth.DelayedVestingAccount{ 307 | BaseVestingAccount: bvacc, 308 | } 309 | } 310 | 311 | appState.Accounts = append(appState.Accounts, app.NewGenesisAccountI(vacc)) 312 | } else { 313 | appState.Accounts = append(appState.Accounts, app.NewGenesisAccount(&acc)) 314 | } 315 | 316 | return appState, nil 317 | } 318 | -------------------------------------------------------------------------------- /x/oracle/keeper/keeper_test.go: -------------------------------------------------------------------------------- 1 | package keeper 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | "github.com/swishlabsco/cosmos-ethereum-bridge/x/oracle/types" 9 | ) 10 | 11 | func TestCreateGetProphecy(t *testing.T) { 12 | ctx, _, keeper, _, validatorAddresses, _ := CreateTestKeepers(t, 0.7, []int64{3, 7}) 13 | 14 | validator1Pow3 := validatorAddresses[0] 15 | 16 | //Test normal Creation 17 | status, err := keeper.ProcessClaim(ctx, types.TestID, validator1Pow3, types.TestString) 18 | require.NoError(t, err) 19 | require.Equal(t, status.StatusText, types.PendingStatusText) 20 | 21 | //Test bad Creation with blank id 22 | status, err = keeper.ProcessClaim(ctx, "", validator1Pow3, types.TestString) 23 | require.Error(t, err) 24 | 25 | //Test bad Creation with blank claim 26 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator1Pow3, "") 27 | require.Error(t, err) 28 | 29 | //Test retrieval 30 | prophecy, err := keeper.GetProphecy(ctx, types.TestID) 31 | require.NoError(t, err) 32 | require.Equal(t, prophecy.ID, types.TestID) 33 | require.Equal(t, prophecy.Status.StatusText, types.PendingStatusText) 34 | require.Equal(t, prophecy.ClaimValidators[types.TestString][0], validator1Pow3) 35 | require.Equal(t, prophecy.ValidatorClaims[validator1Pow3.String()], types.TestString) 36 | } 37 | 38 | func TestBadConsensusForOracle(t *testing.T) { 39 | _, _, _, _, _, err := CreateTestKeepers(t, 0, []int64{10}) 40 | require.Error(t, err) 41 | require.True(t, strings.Contains(err.Error(), "consensus proportion of validator staking power must be > 0 and <= 1")) 42 | 43 | _, _, _, _, _, err = CreateTestKeepers(t, 1.2, []int64{10}) 44 | require.Error(t, err) 45 | require.True(t, strings.Contains(err.Error(), "consensus proportion of validator staking power must be > 0 and <= 1")) 46 | } 47 | 48 | func TestBadMsgs(t *testing.T) { 49 | ctx, _, keeper, _, validatorAddresses, err := CreateTestKeepers(t, 0.6, []int64{3, 3}) 50 | validator1Pow3 := validatorAddresses[0] 51 | 52 | //Test empty claim 53 | status, err := keeper.ProcessClaim(ctx, types.TestID, validator1Pow3, "") 54 | require.Error(t, err) 55 | require.Equal(t, status.FinalClaim, "") 56 | require.True(t, strings.Contains(err.Error(), "Claim cannot be empty string")) 57 | 58 | //Test normal Creation 59 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator1Pow3, types.TestString) 60 | require.NoError(t, err) 61 | require.Equal(t, status.StatusText, types.PendingStatusText) 62 | 63 | //Test duplicate message 64 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator1Pow3, types.TestString) 65 | require.Error(t, err) 66 | require.True(t, strings.Contains(err.Error(), "Already processed message from validator for this id")) 67 | 68 | //Test second but non duplicate message 69 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator1Pow3, types.AlternateTestString) 70 | require.Error(t, err) 71 | require.True(t, strings.Contains(err.Error(), "Already processed message from validator for this id")) 72 | 73 | } 74 | 75 | func TestSuccessfulProphecy(t *testing.T) { 76 | ctx, _, keeper, _, validatorAddresses, err := CreateTestKeepers(t, 0.6, []int64{3, 3, 4}) 77 | validator1Pow3 := validatorAddresses[0] 78 | validator2Pow3 := validatorAddresses[1] 79 | validator3Pow4 := validatorAddresses[2] 80 | 81 | //Test first claim 82 | status, err := keeper.ProcessClaim(ctx, types.TestID, validator1Pow3, types.TestString) 83 | require.NoError(t, err) 84 | require.Equal(t, status.StatusText, types.PendingStatusText) 85 | 86 | //Test second claim completes and finalizes to success 87 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator2Pow3, types.TestString) 88 | require.NoError(t, err) 89 | require.Equal(t, status.StatusText, types.SuccessStatusText) 90 | require.Equal(t, status.FinalClaim, types.TestString) 91 | 92 | //Test third claim not possible 93 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator3Pow4, types.TestString) 94 | require.Error(t, err) 95 | require.True(t, strings.Contains(err.Error(), "Prophecy already finalized")) 96 | } 97 | 98 | func TestSuccessfulProphecyWithDisagreement(t *testing.T) { 99 | ctx, _, keeper, _, validatorAddresses, err := CreateTestKeepers(t, 0.6, []int64{3, 3, 4}) 100 | validator1Pow3 := validatorAddresses[0] 101 | validator2Pow3 := validatorAddresses[1] 102 | validator3Pow4 := validatorAddresses[2] 103 | 104 | //Test first claim 105 | status, err := keeper.ProcessClaim(ctx, types.TestID, validator1Pow3, types.TestString) 106 | require.NoError(t, err) 107 | require.Equal(t, status.StatusText, types.PendingStatusText) 108 | 109 | //Test second disagreeing claim processed fine 110 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator2Pow3, types.AlternateTestString) 111 | require.NoError(t, err) 112 | require.Equal(t, status.StatusText, types.PendingStatusText) 113 | 114 | //Test third claim agrees and finalizes to success 115 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator3Pow4, types.TestString) 116 | require.NoError(t, err) 117 | require.Equal(t, status.StatusText, types.SuccessStatusText) 118 | require.Equal(t, status.FinalClaim, types.TestString) 119 | } 120 | 121 | func TestFailedProphecy(t *testing.T) { 122 | ctx, _, keeper, _, validatorAddresses, err := CreateTestKeepers(t, 0.6, []int64{3, 3, 4}) 123 | validator1Pow3 := validatorAddresses[0] 124 | validator2Pow3 := validatorAddresses[1] 125 | validator3Pow4 := validatorAddresses[2] 126 | 127 | //Test first claim 128 | status, err := keeper.ProcessClaim(ctx, types.TestID, validator1Pow3, types.TestString) 129 | require.NoError(t, err) 130 | require.Equal(t, status.StatusText, types.PendingStatusText) 131 | 132 | //Test second disagreeing claim processed fine 133 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator2Pow3, types.AlternateTestString) 134 | require.NoError(t, err) 135 | require.Equal(t, status.StatusText, types.PendingStatusText) 136 | require.Equal(t, status.FinalClaim, "") 137 | 138 | //Test third disagreeing claim processed fine and prophecy fails 139 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator3Pow4, types.AnotherAlternateTestString) 140 | require.NoError(t, err) 141 | require.Equal(t, status.StatusText, types.FailedStatusText) 142 | require.Equal(t, status.FinalClaim, "") 143 | } 144 | 145 | func TestPower(t *testing.T) { 146 | //Testing with 2 validators but one has high enough power to overrule 147 | ctx, _, keeper, _, validatorAddresses, err := CreateTestKeepers(t, 0.7, []int64{3, 7}) 148 | validator1Pow3 := validatorAddresses[0] 149 | validator2Pow7 := validatorAddresses[1] 150 | 151 | //Test first claim 152 | status, err := keeper.ProcessClaim(ctx, types.TestID, validator1Pow3, types.TestString) 153 | require.NoError(t, err) 154 | require.Equal(t, status.StatusText, types.PendingStatusText) 155 | 156 | //Test second disagreeing claim processed fine and finalized to its bytes 157 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator2Pow7, types.AlternateTestString) 158 | require.NoError(t, err) 159 | require.Equal(t, status.StatusText, types.SuccessStatusText) 160 | require.Equal(t, status.FinalClaim, types.AlternateTestString) 161 | 162 | //Test alternate power setup with validators of 5/4/3/9 and total power 22 and 12/21 required 163 | ctx, _, keeper, _, validatorAddresses, err = CreateTestKeepers(t, 0.571, []int64{5, 4, 3, 9}) 164 | validator1Pow5 := validatorAddresses[0] 165 | validator2Pow4 := validatorAddresses[1] 166 | validator3Pow3 := validatorAddresses[2] 167 | validator4Pow9 := validatorAddresses[3] 168 | 169 | //Test claim by v1 170 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator1Pow5, types.TestString) 171 | require.NoError(t, err) 172 | require.Equal(t, status.StatusText, types.PendingStatusText) 173 | 174 | //Test claim by v2 175 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator2Pow4, types.TestString) 176 | require.NoError(t, err) 177 | require.Equal(t, status.StatusText, types.PendingStatusText) 178 | 179 | //Test alternate claim by v4 180 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator4Pow9, types.AlternateTestString) 181 | require.NoError(t, err) 182 | require.Equal(t, status.StatusText, types.PendingStatusText) 183 | 184 | //Test finalclaim by v3 185 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator3Pow3, types.TestString) 186 | require.NoError(t, err) 187 | require.Equal(t, status.StatusText, types.SuccessStatusText) 188 | require.Equal(t, status.FinalClaim, types.TestString) 189 | } 190 | 191 | func TestMultipleProphecies(t *testing.T) { 192 | //Test multiple prophecies running in parallel work fine as expected 193 | ctx, _, keeper, _, validatorAddresses, err := CreateTestKeepers(t, 0.7, []int64{3, 7}) 194 | validator1Pow3 := validatorAddresses[0] 195 | validator2Pow7 := validatorAddresses[1] 196 | 197 | //Test claim on first id with first validator 198 | status, err := keeper.ProcessClaim(ctx, types.TestID, validator1Pow3, types.TestString) 199 | require.NoError(t, err) 200 | require.Equal(t, status.StatusText, types.PendingStatusText) 201 | 202 | //Test claim on second id with second validator 203 | status, err = keeper.ProcessClaim(ctx, types.AlternateTestID, validator2Pow7, types.AlternateTestString) 204 | require.NoError(t, err) 205 | require.Equal(t, status.StatusText, types.SuccessStatusText) 206 | require.Equal(t, status.FinalClaim, types.AlternateTestString) 207 | 208 | //Test claim on first id with second validator 209 | status, err = keeper.ProcessClaim(ctx, types.TestID, validator2Pow7, types.TestString) 210 | require.NoError(t, err) 211 | require.Equal(t, status.StatusText, types.SuccessStatusText) 212 | require.Equal(t, status.FinalClaim, types.TestString) 213 | 214 | //Test claim on second id with first validator 215 | status, err = keeper.ProcessClaim(ctx, types.AlternateTestID, validator1Pow3, types.AlternateTestString) 216 | require.Error(t, err) 217 | require.True(t, strings.Contains(err.Error(), "Prophecy already finalized")) 218 | } 219 | 220 | func TestNonValidator(t *testing.T) { 221 | //Test multiple prophecies running in parallel work fine as expected 222 | ctx, _, keeper, _, _, _ := CreateTestKeepers(t, 0.7, []int64{3, 7}) 223 | _, validatorAddresses := CreateTestAddrs(10) 224 | inActiveValidatorAddress := validatorAddresses[9] 225 | 226 | //Test claim on first id with first validator 227 | status, err := keeper.ProcessClaim(ctx, types.TestID, inActiveValidatorAddress, types.TestString) 228 | require.Error(t, err) 229 | require.True(t, strings.Contains(err.Error(), "Claim must be made by actively bonded validator")) 230 | require.Equal(t, status.StatusText, "") 231 | } 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ETH Bridge Zone 2 | 3 | [![CircleCI](https://circleci.com/gh/swishlabsco/cosmos-ethereum-bridge/tree/master.svg?style=svg)](https://circleci.com/gh/swishlabsco/cosmos-ethereum-bridge/tree/master) 4 | 5 | ## Project Summary 6 | Unidirectional Peggy is the starting point for cross chain value transfers from the Ethereum blockchain to Cosmos-SDK based blockchains as part of the Ethereum Cosmos Bridge project. The system accepts incoming transfers of Ethereum tokens on an Ethereum smart contract, locking them while the transaction is validated and equitable funds issued to the intended recipient on the Cosmos bridge chain. 7 | 8 | ## Project Background 9 | We are hoping to create a closed system for intra network transfers of cryptocurrency between blockchains, spearheaded by a proof-of-concept which enables secured transactions between Ethereum and Cosmos. 10 | 11 | ## Ethereum Cosmos Bridge Architecture 12 | Unidirectional Peggy focuses on core features for unidirectional transfers. This prototype includes functionality to safely lock and unlock Ethereum, and mint corresponding representative tokens on the Cosmos chain. 13 | 14 | The architecture consists of 4 parts. Each part, and the logical flow of operations is described below. 15 | 16 | ### The smart contracts 17 | First, the smart contract is deployed to an Ethereum network. A user can then send Ethereum to that smart contract to lock up their Ethereum and trigger the transfer flow. 18 | 19 | In this prototype, the system is managed by the contract's deployer, designated internally as the relayer, a trusted third-party which can unlock funds and return them their original sender. If the contract’s balances under threat, the relayer can pause the system, temporarily preventing users from depositing additional funds. 20 | 21 | The Peggy Smart Contract is deployed on the Ropsten testnet at address: 0x3de4ef81Ba6243A60B0a32d3BCeD4173b6EA02bb. More details on the smart contracts and usage can be found in the ethereum-contracts folder. 22 | 23 | ### The Relayer 24 | The Relayer is a service which interfaces with both blockchains, allowing validators to attest on the Cosmos blockchain that specific events on the Ethereum blockchain have occurred. Through the Relayer service, validators witness the events and submit proof in the form of signed hashes to the Cosmos based modules, which are responsible for aggregating and tallying the Validators’ signatures and their respective signing power. 25 | 26 | The Relayer process is as follows: 27 | - continually listen for a `LogLock` event 28 | - when an event is seen, parse information associated with the Ethereum transaction 29 | - uses this information to build an unsigned Cosmos transaction 30 | - signs and send this transaction to Tendermint. 31 | 32 | ### The EthBridge Module 33 | The EthBridge module is a Cosmos-SDK module that is responsible for receiving and decoding transactions involving Ethereum Bridge claims and for processing the result of a successful claim. 34 | 35 | The process is as follows: 36 | - A transaction with a message for the EthBridge module is received 37 | - The message is decoded and transformed into a generic, non-Ethereum specific Oracle claim 38 | - The oracle claim is given a unique ID based on the nonce from the ethereum transaction 39 | - The generic claim is forwarded to the Oracle module. 40 | 41 | The EthBridge module will resume later if the claim succeeds. 42 | 43 | ### The Oracle Module 44 | The Oracle module is intended to be a more generic oracle module that can take arbitrary claims from different validators, hold onto them and perform consensus on those claims once a certain threshold is reached. In this project it is used to find consensus on claims about activity on an Ethereum chain, but it is designed and intended to be able to be used for any other kinds of oracle-like functionality in future (eg: claims about the weather). 45 | 46 | The process is as follows: 47 | - A claim is received from another module (EthBridge in this case) 48 | - That claim is checked, along with other past claims from other validators with the same unique ID 49 | - Once a threshold of stake of the active Tendermint validator set is claiming the same thing, the claim is updated to be successful 50 | - If a threshold of stake of the active Tendermint validator set disagrees, the claim is updated to be a failure 51 | - The status of the claim is returned to the module that provided the claim. 52 | 53 | ### The EthBridge Module (Part 2) 54 | The EthBridge module also contains logic for how a result should be processed. 55 | 56 | The process is as follows: 57 | - Once a claim has been processed by the Oracle, the status is returned 58 | - If the claim is successful, new tokens representing Ethereum are minted via the Bank module 59 | 60 | ### Architecture Diagram 61 | ![peggyarchitecturediagram](./ethbridge.jpg) 62 | 63 | ## Example application 64 | These modules can be added to any Cosmos-SDK based chain, but a demo application/blockchain is provided with example code for how to integrate them. It can be installed and built as follows: 65 | 66 | ``` 67 | # Clone the repository 68 | mkdir -p $GOPATH/src/github.com/swishlabsco 69 | cd $GOPATH/src/github.com/swishlabsco 70 | git clone https://github.com/swishlabsco/cosmos-ethereum-bridge 71 | cd cosmos-ethereum-bridge && git checkout master 72 | 73 | # Install dep, as well as your dependencies 74 | make get_tools 75 | dep ensure -v 76 | go get -u github.com/kardianos/govendor 77 | 78 | # Fetch the C file dependencies (this is a manual hack, as dep does not support pulling non-golang files used in dependencies) 79 | govendor fetch -tree github.com/ethereum/go-ethereum/crypto/secp256k1 80 | 81 | # Install the app into your $GOBIN 82 | make install 83 | 84 | # Now you should be able to run the following commands, confirming the build is successful: 85 | ebd help 86 | ebcli help 87 | ebrelayer help 88 | ``` 89 | 90 | ## Running and testing the application 91 | 92 | First, initialize a chain and create accounts to test sending of a random token. 93 | 94 | ``` 95 | # Initialize the genesis.json file that will help you to bootstrap the network 96 | ebd init --chain-id=testing 97 | 98 | # Create a key to hold your validator account and for another test account 99 | ebcli keys add validator 100 | # Enter password 101 | 102 | ebcli keys add testuser 103 | # Enter password 104 | 105 | ebd add-genesis-account $(ebcli keys show validator -a) 1000000000stake,1000000000atom 106 | 107 | # Now its safe to start `ebd` 108 | ebd start 109 | 110 | # Then, wait 10 seconds and in another terminal window, test things are ok by sending 10 tok tokens from the validator to the testuser 111 | ebcli tx send $(ebcli keys show testuser -a) 10stake --from=validator --chain-id=testing --yes 112 | 113 | # Confirm token balances have changed appropriately 114 | ebcli query account $(ebcli keys show validator -a) --trust-node 115 | ebcli query account $(ebcli keys show testuser -a) --trust-node 116 | 117 | # Next, setup the staking module prerequisites 118 | # First, create a validator and stake 119 | ebcli tx staking create-validator \ 120 | --amount=100000000stake \ 121 | --pubkey=$(ebd tendermint show-validator) \ 122 | --moniker="test_moniker" \ 123 | --chain-id=testing \ 124 | --commission-rate="0.10" \ 125 | --commission-max-rate="0.20" \ 126 | --commission-max-change-rate="0.01" \ 127 | --min-self-delegation="1" \ 128 | --gas=200000 \ 129 | --gas-prices="0.001stake" \ 130 | --from=validator 131 | 132 | # Then wait 10 seconds then confirm your validator was created correctly, and has become Bonded status 133 | ebcli query staking validators --trust-node 134 | 135 | # See the help for the ethbridge make claim function 136 | ebcli tx ethbridge make-claim --help 137 | 138 | # Now you can test out the ethbridge module by submitting a claim for an ethereum prophecy 139 | # Make a bridge claim (Ethereum prophecies are stored on the blockchain with an identifier created by concatenating the nonce and sender address) 140 | ebcli tx ethbridge make-claim 0 0x7B95B6EC7EbD73572298cEf32Bb54FA408207359 $(ebcli keys show testuser -a) $(ebcli keys show validator -a) 3eth --from validator --chain-id testing --yes 141 | 142 | # Then read the prophecy to confirm it was created with the claim added 143 | ebcli query ethbridge get-prophecy 0 0x7B95B6EC7EbD73572298cEf32Bb54FA408207359 --trust-node 144 | 145 | # And finally, confirm that the prophecy was successfully processed and that new eth was minted to the testuser address 146 | ebcli query account $(ebcli keys show testuser -a) --trust-node 147 | 148 | ``` 149 | 150 | ## Using the application from rest-server 151 | 152 | First, run the cli rest-server 153 | 154 | ``` 155 | ebcli rest-server --trust-node 156 | ``` 157 | 158 | An api collection for Postman (https://www.getpostman.com/) is provided [here](./cosmos-ethereum-bridge.postman_collection.json) which documents some API endpoints and can be used to interact with it. 159 | Note: For checking account details/balance, you will need to change the cosmos addresses in the URLs, params and body to match the addresses you generated that you want to check. 160 | 161 | ## Running the relayer service 162 | 163 | For automated relaying, there is a relayer service that can be run that will automatically watch and relay events. 164 | 165 | ``` 166 | # Check ebrelayer connection to ebd 167 | ebrelayer status 168 | 169 | # Initialize the Relayer service for automatic claim processing 170 | ebrelayer init testing wss://ropsten.infura.io/ws 3de4ef81Ba6243A60B0a32d3BCeD4173b6EA02bb "LogLock(bytes32,address,bytes,address,uint256,uint256)" validator 171 | 172 | # Enter password and press enter 173 | # You should see a message like: Started ethereum websocket... and Subscribed to contract events... 174 | ``` 175 | 176 | The relayer will now watch the contract on Ropsten and create a claim whenever it detects a lock event. 177 | 178 | ## Using the bridge 179 | 180 | With the application set up and the relayer running, you can now use Peggy by sending a lock transaction to the smart contract. You can do this from any Ethereum wallet/client that supports smart contract transactions. 181 | 182 | The easiest way to do this for now, assuming you have Metamask setup for Ropsten in the browser is to use remix or mycrypto as the frontend, for example: 183 | - 1. Go to remix.ethereum.org 184 | - 2. Compile Peggy.sol with solc v0.5.0 185 | - 3. Set the environment as Injected Web3 Ropsten 186 | - 4. On 'Run' tab, select Peggy and enter "0x3de4ef81Ba6243A60B0a32d3BCeD4173b6EA02bb" in 'At Address' field 187 | - 5. Select 'At Address' to load the deployed contract 188 | - 6. Enter the following for the variables under function lock(): 189 | _recipient = [HASHED_COSMOS_RECIPIENT_ADDRESS] *(for testuser cosmos1pjtgu0vau2m52nrykdpztrt887aykue0hq7dfh, enter "0x636f736d6f7331706a74677530766175326d35326e72796b64707a74727438383761796b756530687137646668")* 190 | _token = [DEPLOYED_TOKEN_ADDRESS] *(erc20 not currently supported, enter "0x0000000000000000000000000000000000000000" for ethereum)* 191 | _amount = [WEI_AMOUNT] 192 | - 7. Enter the same number from _amount as the transaction's value (in wei) 193 | - 8. Select "transact" to send the lock() transaction 194 | 195 | ## Using the modules in other projects 196 | 197 | The ethbridge and oracle modules can be used in other cosmos-sdk applications by copying them into your application's modules folders and including them in the same way as in the example application. Each module may be moved to its own repo or integrated into the core Cosmos-SDK in future, for easier usage. 198 | 199 | There are 2 nuances you need to be aware of when using these modules in other Cosmos-SDK projects. 200 | - A specific version of golang.org/x/crypto (ie tendermint/crypto) is needed for compatability with go-ethereum. See the Gopkg.toml for constraint details. There is an open pull request to tendermint/crypto to add compatbility, but until that is merged you need to use the customized version (https://github.com/tendermint/crypto/pull/1) 201 | - The govendor steps in the application as above are needed 202 | 203 | For instructions on building and deploying the smart contracts, see the README in their folder. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Cosmos-SDK 2 | License: Apache2.0 3 | 4 | Apache License 5 | Version 2.0, January 2004 6 | http://www.apache.org/licenses/ 7 | 8 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 9 | 10 | 1. Definitions. 11 | 12 | "License" shall mean the terms and conditions for use, reproduction, 13 | and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by 16 | the copyright owner that is granting the License. 17 | 18 | "Legal Entity" shall mean the union of the acting entity and all 19 | other entities that control, are controlled by, or are under common 20 | control with that entity. For the purposes of this definition, 21 | "control" means (i) the power, direct or indirect, to cause the 22 | direction or management of such entity, whether by contract or 23 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 24 | outstanding shares, or (iii) beneficial ownership of such entity. 25 | 26 | "You" (or "Your") shall mean an individual or Legal Entity 27 | exercising permissions granted by this License. 28 | 29 | "Source" form shall mean the preferred form for making modifications, 30 | including but not limited to software source code, documentation 31 | source, and configuration files. 32 | 33 | "Object" form shall mean any form resulting from mechanical 34 | transformation or translation of a Source form, including but 35 | not limited to compiled object code, generated documentation, 36 | and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or 39 | Object form, made available under the License, as indicated by a 40 | copyright notice that is included in or attached to the work 41 | (an example is provided in the Appendix below). 42 | 43 | "Derivative Works" shall mean any work, whether in Source or Object 44 | form, that is based on (or derived from) the Work and for which the 45 | editorial revisions, annotations, elaborations, or other modifications 46 | represent, as a whole, an original work of authorship. For the purposes 47 | of this License, Derivative Works shall not include works that remain 48 | separable from, or merely link (or bind by name) to the interfaces of, 49 | the Work and Derivative Works thereof. 50 | 51 | "Contribution" shall mean any work of authorship, including 52 | the original version of the Work and any modifications or additions 53 | to that Work or Derivative Works thereof, that is intentionally 54 | submitted to Licensor for inclusion in the Work by the copyright owner 55 | or by an individual or Legal Entity authorized to submit on behalf of 56 | the copyright owner. For the purposes of this definition, "submitted" 57 | means any form of electronic, verbal, or written communication sent 58 | to the Licensor or its representatives, including but not limited to 59 | communication on electronic mailing lists, source code control systems, 60 | and issue tracking systems that are managed by, or on behalf of, the 61 | Licensor for the purpose of discussing and improving the Work, but 62 | excluding communication that is conspicuously marked or otherwise 63 | designated in writing by the copyright owner as "Not a Contribution." 64 | 65 | "Contributor" shall mean Licensor and any individual or Legal Entity 66 | on behalf of whom a Contribution has been received by Licensor and 67 | subsequently incorporated within the Work. 68 | 69 | 2. Grant of Copyright License. Subject to the terms and conditions of 70 | this License, each Contributor hereby grants to You a perpetual, 71 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 72 | copyright license to reproduce, prepare Derivative Works of, 73 | publicly display, publicly perform, sublicense, and distribute the 74 | Work and such Derivative Works in Source or Object form. 75 | 76 | 3. Grant of Patent License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | (except as stated in this section) patent license to make, have made, 80 | use, offer to sell, sell, import, and otherwise transfer the Work, 81 | where such license applies only to those patent claims licensable 82 | by such Contributor that are necessarily infringed by their 83 | Contribution(s) alone or by combination of their Contribution(s) 84 | with the Work to which such Contribution(s) was submitted. If You 85 | institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work 87 | or a Contribution incorporated within the Work constitutes direct 88 | or contributory patent infringement, then any patent licenses 89 | granted to You under this License for that Work shall terminate 90 | as of the date such litigation is filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the 93 | Work or Derivative Works thereof in any medium, with or without 94 | modifications, and in Source or Object form, provided that You 95 | meet the following conditions: 96 | 97 | (a) You must give any other recipients of the Work or 98 | Derivative Works a copy of this License; and 99 | 100 | (b) You must cause any modified files to carry prominent notices 101 | stating that You changed the files; and 102 | 103 | (c) You must retain, in the Source form of any Derivative Works 104 | that You distribute, all copyright, patent, trademark, and 105 | attribution notices from the Source form of the Work, 106 | excluding those notices that do not pertain to any part of 107 | the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" text file as part of its 110 | distribution, then any Derivative Works that You distribute must 111 | include a readable copy of the attribution notices contained 112 | within such NOTICE file, excluding those notices that do not 113 | pertain to any part of the Derivative Works, in at least one 114 | of the following places: within a NOTICE text file distributed 115 | as part of the Derivative Works; within the Source form or 116 | documentation, if provided along with the Derivative Works; or, 117 | within a display generated by the Derivative Works, if and 118 | wherever such third-party notices normally appear. The contents 119 | of the NOTICE file are for informational purposes only and 120 | do not modify the License. You may add Your own attribution 121 | notices within Derivative Works that You distribute, alongside 122 | or as an addendum to the NOTICE text from the Work, provided 123 | that such additional attribution notices cannot be construed 124 | as modifying the License. 125 | 126 | You may add Your own copyright statement to Your modifications and 127 | may provide additional or different license terms and conditions 128 | for use, reproduction, or distribution of Your modifications, or 129 | for any such Derivative Works as a whole, provided Your use, 130 | reproduction, and distribution of the Work otherwise complies with 131 | the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, 134 | any Contribution intentionally submitted for inclusion in the Work 135 | by You to the Licensor shall be under the terms and conditions of 136 | this License, without any additional terms or conditions. 137 | Notwithstanding the above, nothing herein shall supersede or modify 138 | the terms of any separate license agreement you may have executed 139 | with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. This License does not grant permission to use the trade 142 | names, trademarks, service marks, or product names of the Licensor, 143 | except as required for reasonable and customary use in describing the 144 | origin of the Work and reproducing the content of the NOTICE file. 145 | 146 | 7. Disclaimer of Warranty. Unless required by applicable law or 147 | agreed to in writing, Licensor provides the Work (and each 148 | Contributor provides its Contributions) on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 150 | implied, including, without limitation, any warranties or conditions 151 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 152 | PARTICULAR PURPOSE. You are solely responsible for determining the 153 | appropriateness of using or redistributing the Work and assume any 154 | risks associated with Your exercise of permissions under this License. 155 | 156 | 8. Limitation of Liability. In no event and under no legal theory, 157 | whether in tort (including negligence), contract, or otherwise, 158 | unless required by applicable law (such as deliberate and grossly 159 | negligent acts) or agreed to in writing, shall any Contributor be 160 | liable to You for damages, including any direct, indirect, special, 161 | incidental, or consequential damages of any character arising as a 162 | result of this License or out of the use or inability to use the 163 | Work (including but not limited to damages for loss of goodwill, 164 | work stoppage, computer failure or malfunction, or any and all 165 | other commercial damages or losses), even if such Contributor 166 | has been advised of the possibility of such damages. 167 | 168 | 9. Accepting Warranty or Additional Liability. While redistributing 169 | the Work or Derivative Works thereof, You may choose to offer, 170 | and charge a fee for, acceptance of support, warranty, indemnity, 171 | or other liability obligations and/or rights consistent with this 172 | License. However, in accepting such obligations, You may act only 173 | on Your own behalf and on Your sole responsibility, not on behalf 174 | of any other Contributor, and only if You agree to indemnify, 175 | defend, and hold each Contributor harmless for any liability 176 | incurred by, or claims asserted against, such Contributor by reason 177 | of your accepting any such warranty or additional liability. 178 | 179 | END OF TERMS AND CONDITIONS 180 | 181 | APPENDIX: How to apply the Apache License to your work. 182 | 183 | To apply the Apache License to your work, attach the following 184 | boilerplate notice, with the fields enclosed by brackets "{}" 185 | replaced with your own identifying information. (Don't include 186 | the brackets!) The text should be enclosed in the appropriate 187 | comment syntax for the file format. We also recommend that a 188 | file or class name and description of purpose be included on the 189 | same "printed page" as the copyright notice for easier 190 | identification within third-party archives. 191 | 192 | Copyright 2016 All in Bits, Inc 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | -------------------------------------------------------------------------------- /ethereum-contracts/test/test_peggy.js: -------------------------------------------------------------------------------- 1 | const Peggy = artifacts.require('Peggy'); 2 | const TestToken = artifacts.require('TestToken'); 3 | 4 | const Web3Utils = require('web3-utils'); 5 | const EVMRevert = 'revert'; 6 | const BigNumber = web3.BigNumber; 7 | 8 | require('chai') 9 | .use(require('chai-as-promised')) 10 | .use(require('chai-bignumber')(BigNumber)) 11 | .should(); 12 | 13 | contract('Peggy', function (accounts) { 14 | 15 | const relayer = accounts[0]; 16 | 17 | const userOne = accounts[1]; 18 | const userTwo = accounts[2]; 19 | const userThree = accounts[3]; 20 | 21 | describe('Peggy smart contract deployment', function(){ 22 | 23 | beforeEach(async function() { 24 | this.peggy = await Peggy.new(); 25 | }); 26 | 27 | it('should deploy the peggy contract with the correct parameters', async function () { 28 | this.peggy.should.exist; 29 | 30 | const peggyRelayer = (await this.peggy.relayer()); 31 | peggyRelayer.should.be.equal(relayer); 32 | }); 33 | 34 | }); 35 | 36 | describe('Locking funds', function(){ 37 | 38 | beforeEach(async function() { 39 | this.peggy = await Peggy.new(); 40 | this.ethereumToken = '0x0000000000000000000000000000000000000000'; 41 | this.weiAmount = web3.utils.toWei("0.25", "ether"); 42 | this.recipient = web3.utils.utf8ToHex('985cfkop78sru7gfud4wce83kuc9rmw89rqtzmy'); 43 | this.gasForLock = 300000; // 300,000 Gwei 44 | 45 | //Load user account with tokens for testing 46 | this.token = await TestToken.new(); 47 | await this.token.mint(userOne, 1000, { from: relayer }).should.be.fulfilled; 48 | }); 49 | 50 | it('should not allow a user to send ethereum directly to the contract', async function () { 51 | await this.peggy.send(this.weiAmount, { from: userOne}).should.be.rejectedWith(EVMRevert); 52 | }); 53 | 54 | it('should not allow users to lock funds if the contract is paused', async function () { 55 | //Confirm that the processor contract is paused 56 | await this.peggy.pauseLocking({ from:relayer }).should.be.fulfilled; 57 | 58 | const depositStatus = await this.peggy.active(); 59 | depositStatus.should.be.equal(false); 60 | 61 | //User attempt to lock ethereum/erc20 62 | await this.token.approve(this.peggy.address, 100, { from: userOne }).should.be.fulfilled; 63 | await this.peggy.lock(this.recipient, this.token.address, 100, { from: userOne, gas: this.gasForLock }).should.be.rejectedWith(EVMRevert); 64 | }); 65 | 66 | it('should allow users to lock erc20 tokens if it meets validation requirements', async function () { 67 | //Confirm that the contract is active 68 | const depositStatus = await this.peggy.active(); 69 | depositStatus.should.be.equal(true); 70 | 71 | await this.token.approve(this.peggy.address, 100, { from: userOne }).should.be.fulfilled; 72 | await this.peggy.lock(this.recipient, this.token.address, 100, { from: userOne, gas: this.gasForLock }).should.be.fulfilled; 73 | 74 | //Get the contract and user token balance after the rescue 75 | const peggyBalance = Number(await this.token.balanceOf(this.peggy.address)); 76 | const userBalance = Number(await this.token.balanceOf(userOne)); 77 | 78 | //Confirm that the tokens have been locked 79 | peggyBalance.should.be.bignumber.equal(100); 80 | userBalance.should.be.bignumber.equal(900); 81 | }); 82 | 83 | it('should allow users to lock ethereum if it meets validation requirements', async function () { 84 | //Confirm that the contract is active 85 | const depositStatus = await this.peggy.active(); 86 | depositStatus.should.be.equal(true); 87 | 88 | await this.peggy.lock(this.recipient, this.ethereumToken, this.weiAmount, { from: userOne, value: this.weiAmount, gas: this.gasForLock }).should.be.fulfilled; 89 | 90 | const contractBalanceWei = await web3.eth.getBalance(this.peggy.address); 91 | const contractBalance = web3.utils.fromWei(contractBalanceWei, "ether"); 92 | 93 | contractBalance.should.be.bignumber.equal(web3.utils.fromWei(this.weiAmount, "ether")); 94 | 95 | }); 96 | 97 | it('should emit an event upon lock containing the new ecrow\'s information', async function () { 98 | const userBalance = Number(await this.token.balanceOf(userOne)); 99 | userBalance.should.be.bignumber.equal(1000); 100 | 101 | await this.token.approve(this.peggy.address, 100, { from: userOne }).should.be.fulfilled; 102 | 103 | //Get the event logs of a token deposit 104 | const expectedId = await this.peggy.lock.call(this.recipient, this.token.address, 100, { from: userOne, gas: this.gasForLock }).should.be.fulfilled; 105 | const {logs} = await this.peggy.lock(this.recipient, this.token.address, 100, { from: userOne, gas: this.gasForLock }).should.be.fulfilled; 106 | const event = logs.find(e => e.event === 'LogLock'); 107 | 108 | event.args._id.should.be.equal(expectedId); 109 | event.args._to.should.be.equal(this.recipient); 110 | event.args._token.should.be.equal(this.token.address); 111 | Number(event.args._value).should.be.bignumber.equal(100); 112 | Number(event.args._nonce).should.be.bignumber.equal(1); 113 | }); 114 | 115 | }); 116 | 117 | describe('Access to information', function(){ 118 | 119 | const cosmosAddr = '77m5cfkop78sruko3ud4wjp83kuc9rmw15rqtzlp'; 120 | 121 | beforeEach(async function() { 122 | this.peggy = await Peggy.new(); 123 | this.ethereumToken = '0x0000000000000000000000000000000000000000'; 124 | this.weiAmount = web3.utils.toWei("0.25", "ether"); 125 | this.recipient = web3.utils.utf8ToHex(cosmosAddr); 126 | this.gasForLock = 300000; // 300,000 Gwei 127 | 128 | // Load user account with tokens for testing 129 | this.token = await TestToken.new(); 130 | await this.token.mint(userOne, 100, { from: relayer }).should.be.fulfilled; 131 | 132 | await this.token.approve(this.peggy.address, 100, { from: userOne }).should.be.fulfilled; 133 | this.itemId = await this.peggy.lock.call(this.recipient, this.token.address, 100, { from: userOne, gas: this.gasForLock }).should.be.fulfilled; 134 | await this.peggy.lock(this.recipient, this.token.address, 100, { from: userOne, gas: this.gasForLock }).should.be.fulfilled; 135 | }); 136 | 137 | it('should allow for public viewing of a locked item\'s information', async function () { 138 | //Get the item struct's information 139 | const itemInfo = await this.peggy.viewItem(this.itemId, { from: relayer }).should.be.fulfilled; 140 | 141 | //Parse each attribute 142 | const sender = itemInfo[0]; 143 | const receiver = itemInfo[1]; 144 | const token = itemInfo[2]; 145 | const amount = Number(itemInfo[3]); 146 | const nonce = Number(itemInfo[4]); 147 | 148 | //Confirm that each attribute is correct 149 | sender.should.be.equal(userOne); 150 | receiver.should.be.equal(this.recipient); 151 | token.should.be.equal(this.token.address); 152 | amount.should.be.bignumber.equal(100); 153 | nonce.should.be.bignumber.equal(1); 154 | }); 155 | 156 | it('should correctly encode and decode the intended recipient\'s address', async function () { 157 | //Get the item struct's information 158 | const itemInfo = await this.peggy.viewItem(this.itemId, { from: relayer }).should.be.fulfilled; 159 | 160 | //Decode the stored recipient's address and compare it the original 161 | const receiver = web3.utils.hexToUtf8(itemInfo[1]); 162 | receiver.should.be.equal(cosmosAddr); 163 | }); 164 | 165 | }); 166 | 167 | describe('Unlocking of itemized ethereum', function(){ 168 | 169 | beforeEach(async function() { 170 | this.peggy = await Peggy.new(); 171 | this.ethereumToken = '0x0000000000000000000000000000000000000000'; 172 | this.weiAmount = web3.utils.toWei("0.25", "ether"); 173 | this.recipient = web3.utils.utf8ToHex('cosmosaccaddr985cfkop78sru7gfud4wce83kuc9rmw89rqtzmy'); 174 | this.gasForLock = 300000; // 300,000 Gwei 175 | 176 | // Load user account with tokens for testing 177 | this.token = await TestToken.new(); 178 | await this.token.mint(userOne, 100, { from: relayer }).should.be.fulfilled; 179 | 180 | await this.token.approve(this.peggy.address, 100, { from: userOne }).should.be.fulfilled; 181 | this.itemId = await this.peggy.lock.call(this.recipient, this.token.address, 100, { from: userOne, gas: this.gasForLock }).should.be.fulfilled; 182 | await this.peggy.lock(this.recipient, this.token.address, 100, { from: userOne, gas: this.gasForLock }).should.be.fulfilled; 183 | 184 | }); 185 | 186 | it('should allow the relayer to unlock itemized ethereum', async function () { 187 | const id = await this.peggy.lock.call(this.recipient, this.ethereumToken, this.weiAmount, { from: userOne, value: this.weiAmount, gas: this.gasForLock }).should.be.fulfilled; 188 | await this.peggy.lock(this.recipient, this.ethereumToken, this.weiAmount, { from: userOne, value: this.weiAmount, gas: this.gasForLock }).should.be.fulfilled; 189 | await this.peggy.unlock(id, { from: relayer, gas: this.gasForLock }).should.be.fulfilled; 190 | }); 191 | 192 | it('should allow the relayer to unlock itemized erc20 tokens', async function () { 193 | await this.peggy.unlock(this.itemId, { from: relayer, gas: this.gasForLock }).should.be.fulfilled; 194 | }); 195 | 196 | it('should correctly transfer funds to intended recipient upon unlock', async function () { 197 | //Confirm that the tokens are locked on the contract 198 | const beforePeggyBalance = Number(await this.token.balanceOf(this.peggy.address)); 199 | const beforeUserBalance = Number(await this.token.balanceOf(userOne)); 200 | 201 | beforePeggyBalance.should.be.bignumber.equal(100); 202 | beforeUserBalance.should.be.bignumber.equal(0); 203 | 204 | await this.peggy.unlock(this.itemId, { from: relayer, gas: this.gasForLock }); 205 | 206 | //Confirm that the tokens have been unlocked and transfered 207 | const afterPeggyBalance = Number(await this.token.balanceOf(this.peggy.address)); 208 | const afterUserBalance = Number(await this.token.balanceOf(userOne)); 209 | 210 | afterPeggyBalance.should.be.bignumber.equal(0); 211 | afterUserBalance.should.be.bignumber.equal(100); 212 | }); 213 | 214 | it('should emit an event upon unlock containing the ecrow\'s recipient, token, amount, and nonce', async function () { 215 | //Get the event logs of an unlock 216 | const {logs} = await this.peggy.unlock(this.itemId, { from: relayer, gas: this.gasForLock }); 217 | const event = logs.find(e => e.event === 'LogUnlock'); 218 | 219 | event.args._to.should.be.equal(userOne); 220 | event.args._token.should.be.equal(this.token.address); 221 | Number(event.args._value).should.be.bignumber.equal(100); 222 | Number(event.args._nonce).should.be.bignumber.equal(1); 223 | }); 224 | 225 | it('should update item lock statusit has been unlocked', async function () { 226 | const startingLockStatus = await this.peggy.getStatus(this.itemId); 227 | startingLockStatus.should.be.equal(true); 228 | 229 | await this.peggy.unlock(this.itemId, { from: relayer, gas: this.gasForLock }); 230 | 231 | const endingLockStatus = await this.peggy.getStatus(this.itemId); 232 | endingLockStatus.should.be.equal(false); 233 | 234 | }); 235 | 236 | }); 237 | 238 | describe('Withdrawal of items by sender', function(){ 239 | 240 | beforeEach(async function() { 241 | this.peggy = await Peggy.new(); 242 | this.zeroxToken = '0xE41d2489571d322189246DaFA5ebDe1F4699F498'; 243 | this.ethereumToken = '0x0000000000000000000000000000000000000000'; 244 | this.weiAmount = web3.utils.toWei("0.25", "ether"); 245 | this.recipient = web3.utils.utf8ToHex('cosmosaccaddr985cfkop78sru7gfud4wce83kuc9rmw89rqtzmy'); 246 | this.gasForLock = 300000; // 300,000 Gwei 247 | 248 | // Load user account with tokens for testing 249 | this.token = await TestToken.new(); 250 | await this.token.mint(userOne, 100, { from: relayer }).should.be.fulfilled; 251 | 252 | await this.token.approve(this.peggy.address, 100, { from: userOne }).should.be.fulfilled; 253 | this.itemId = await this.peggy.lock.call(this.recipient, this.token.address, 100, { from: userOne, gas: this.gasForLock }).should.be.fulfilled; 254 | await this.peggy.lock(this.recipient, this.token.address, 100, { from: userOne, gas: this.gasForLock }).should.be.fulfilled; 255 | 256 | //Lower and upper bound to allow results in range of 0.01% 257 | this.lowerBound = 0.9999; 258 | this.upperBound = 1.0001; 259 | 260 | //Set up gas constants 261 | this.gasForLock = 500000; //500,000 Gwei 262 | this.gasForWithdraw = 200000; //200,000 Gwei 263 | this.gasPrice = 200000000000; //From truffle config 264 | }); 265 | 266 | it('should not allow non-senders to withdraw other\'s items', async function () { 267 | await this.peggy.withdraw(this.itemId, { from: userThree, gas: this.gasForLock }).should.be.rejectedWith(EVMRevert); 268 | }); 269 | 270 | it('should allow senders to withdraw their own items', async function () { 271 | await this.peggy.withdraw(this.itemId, { from: userOne, gas: this.gasForLock }).should.be.fulfilled; 272 | }); 273 | 274 | it('should return erc20 to user upon withdrawal of itemized funds', async function () { 275 | //Confirm that the tokens are locked on the contract 276 | const beforePeggyBalance = Number(await this.token.balanceOf(this.peggy.address)); 277 | const beforeUserBalance = Number(await this.token.balanceOf(userOne)); 278 | 279 | beforePeggyBalance.should.be.bignumber.equal(100); 280 | beforeUserBalance.should.be.bignumber.equal(0); 281 | 282 | await this.peggy.withdraw(this.itemId, { from: userOne, gas: this.gasForWithdraw }).should.be.fulfilled; 283 | 284 | //Confirm that the tokens have been unlocked and transfered 285 | const afterPeggyBalance = Number(await this.token.balanceOf(this.peggy.address)); 286 | const afterUserBalance = Number(await this.token.balanceOf(userOne)); 287 | 288 | afterPeggyBalance.should.be.bignumber.equal(0); 289 | afterUserBalance.should.be.bignumber.equal(100); 290 | }); 291 | 292 | it('should return ethereum to user upon withdrawal of itemized funds', async function () { 293 | const id = await this.peggy.lock.call(this.recipient, this.ethereumToken, this.weiAmount, { from: userTwo, value: this.weiAmount, gas: this.gasForLock }).should.be.fulfilled; 294 | await this.peggy.lock(this.recipient, this.ethereumToken, this.weiAmount, { from: userTwo, value: this.weiAmount, gas: this.gasForLock }).should.be.fulfilled; 295 | 296 | //Set up accepted withdrawal balance bounds 297 | const lowestAcceptedBalance = this.weiAmount * this.lowerBound; 298 | const highestAcceptedBalance = this.weiAmount * this.upperBound; 299 | 300 | //Get prior balances of user and peggy contract 301 | const beforeUserBalance = Number(await web3.eth.getBalance(userTwo)); 302 | const beforeContractBalance = Number(await web3.eth.getBalance(this.peggy.address)); 303 | 304 | //Send withdrawal transaction and save gas expenditure 305 | const txHash = await this.peggy.withdraw(id, { from: userTwo, gas: this.gasForWithdraw}).should.be.fulfilled; 306 | const gasCost = this.gasPrice * txHash.receipt.gasUsed; 307 | 308 | //Get balances after withdrawal 309 | const afterUserBalance = Number(await web3.eth.getBalance(userTwo)); 310 | const afterContractBalance = Number(await web3.eth.getBalance(this.peggy.address)); 311 | 312 | //Check user's balances to confirm withdrawal 313 | const userDifference = (afterUserBalance - beforeUserBalance) + gasCost; 314 | userDifference.should.be.bignumber.within(lowestAcceptedBalance, highestAcceptedBalance); 315 | 316 | //Check contracts's balances to confirm withdrawal 317 | const contractDifference = (beforeContractBalance - afterContractBalance); 318 | contractDifference.should.be.bignumber.within(lowestAcceptedBalance, highestAcceptedBalance); 319 | }); 320 | 321 | it('should emit an event upon user withdrawal containing the item\'s id', async function () { 322 | //Get the event logs of a token withdrawl 323 | const {logs} = await this.peggy.withdraw(this.itemId, { from: userOne, gas: this.gasForLock }).should.be.fulfilled; 324 | const event = logs.find(e => e.event === 'LogWithdraw'); 325 | 326 | //Check the event's parameters 327 | event.args._id.should.be.bignumber.equal(this.itemId); 328 | event.args._to.should.be.equal(userOne); 329 | event.args._token.should.be.equal(this.token.address); 330 | Number(event.args._value).should.be.bignumber.equal(100); 331 | Number(event.args._nonce).should.be.bignumber.equal(1); 332 | }); 333 | 334 | }); 335 | 336 | }); --------------------------------------------------------------------------------