diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d6a356f7..af24be8a42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,8 +37,14 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## Unreleased +### State Machine Breaking + - (upgrade) [#2933](https://github.com/evmos/evmos/pull/2933) Update upgrade handler logic for `v20` release. +### Improvements + +- (precompiles) [#2922](https://github.com/evmos/evmos/pull/2922) Add 'VoteWeighted' transaction to gov precompile. + ## [v20.0.0-rc3](https://github.com/evmos/evmos/releases/tag/v20.0.0-rc3) - 2024-10-08 ### State Machine Breaking diff --git a/precompiles/gov/IGov.sol b/precompiles/gov/IGov.sol index 94438792f1..1ab2e5fe3b 100644 --- a/precompiles/gov/IGov.sol +++ b/precompiles/gov/IGov.sol @@ -48,6 +48,12 @@ interface IGov { /// @param option the option for voter event Vote(address indexed voter, uint64 proposalId, uint8 option); + /// @dev VoteWeighted defines an Event emitted when a proposal voted. + /// @param voter the address of the voter + /// @param proposalId the proposal of id + /// @param options the options for voter + event VoteWeighted(address indexed voter, uint64 proposalId, WeightedVoteOption[] options); + /// TRANSACTIONS /// @dev vote defines a method to add a vote on a specific proposal. @@ -63,6 +69,19 @@ interface IGov { string memory metadata ) external returns (bool success); + /// @dev voteWeighted defines a method to add a vote on a specific proposal. + /// @param voter The address of the voter + /// @param proposalId The proposal id + /// @param options The options for voter + /// @param metadata The metadata for voter send + /// @return success Whether the transaction was successful or not + function voteWeighted( + address voter, + uint64 proposalId, + WeightedVoteOption[] calldata options, + string memory metadata + ) external returns (bool success); + /// QUERIES /// @dev getVote returns the vote of a single voter for a diff --git a/precompiles/gov/abi.json b/precompiles/gov/abi.json index 3d3ff4e312..2f1745c484 100644 --- a/precompiles/gov/abi.json +++ b/precompiles/gov/abi.json @@ -28,6 +28,43 @@ "name": "Vote", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "voter", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "proposalId", + "type": "uint64" + }, + { + "components": [ + { + "internalType": "enum VoteOption", + "name": "option", + "type": "uint8" + }, + { + "internalType": "string", + "name": "weight", + "type": "string" + } + ], + "indexed": false, + "internalType": "struct WeightedVoteOption[]", + "name": "options", + "type": "tuple[]" + } + ], + "name": "VoteWeighted", + "type": "event" + }, { "inputs": [ { @@ -221,6 +258,52 @@ ], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "voter", + "type": "address" + }, + { + "internalType": "uint64", + "name": "proposalId", + "type": "uint64" + }, + { + "components": [ + { + "internalType": "enum VoteOption", + "name": "option", + "type": "uint8" + }, + { + "internalType": "string", + "name": "weight", + "type": "string" + } + ], + "internalType": "struct WeightedVoteOption[]", + "name": "options", + "type": "tuple[]" + }, + { + "internalType": "string", + "name": "metadata", + "type": "string" + } + ], + "name": "voteWeighted", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" } ], "bytecode": "0x", diff --git a/precompiles/gov/errors.go b/precompiles/gov/errors.go index 25e79200c9..3a73de092b 100644 --- a/precompiles/gov/errors.go +++ b/precompiles/gov/errors.go @@ -15,4 +15,12 @@ const ( ErrInvalidOption = "invalid option %s " // ErrInvalidMetadata invalid metadata. ErrInvalidMetadata = "invalid metadata %s " + // ErrInvalidWeightedVoteOptions invalid weighted vote options. + ErrInvalidWeightedVoteOptions = "invalid weighted vote options %s " + // ErrInvalidWeightedVoteOption invalid weighted vote option. + ErrInvalidWeightedVoteOption = "invalid weighted vote option %s " + // ErrInvalidWeightedVoteOptionType invalid weighted vote option type. + ErrInvalidWeightedVoteOptionType = "invalid weighted vote option type %s " + // ErrInvalidWeightedVoteOptionWeight invalid weighted vote option weight. + ErrInvalidWeightedVoteOptionWeight = "invalid weighted vote option weight %s " ) diff --git a/precompiles/gov/events.go b/precompiles/gov/events.go index 1cf53ab6f9..e5baa098e1 100644 --- a/precompiles/gov/events.go +++ b/precompiles/gov/events.go @@ -5,7 +5,6 @@ package gov import ( sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -17,6 +16,8 @@ import ( const ( // EventTypeVote defines the event type for the gov VoteMethod transaction. EventTypeVote = "Vote" + // EventTypeVoteWeighted defines the event type for the gov VoteWeightedMethod transaction. + EventTypeVoteWeighted = "VoteWeighted" ) // EmitVoteEvent creates a new event emitted on a Vote transaction. @@ -50,3 +51,35 @@ func (p Precompile) EmitVoteEvent(ctx sdk.Context, stateDB vm.StateDB, voterAddr return nil } + +// EmitVoteWeightedEvent creates a new event emitted on a VoteWeighted transaction. +func (p Precompile) EmitVoteWeightedEvent(ctx sdk.Context, stateDB vm.StateDB, voterAddress common.Address, proposalID uint64, options WeightedVoteOptions) error { + // Prepare the event topics + event := p.ABI.Events[EventTypeVoteWeighted] + topics := make([]common.Hash, 2) + + // The first topic is always the signature of the event. + topics[0] = event.ID + + var err error + topics[1], err = cmn.MakeTopic(voterAddress) + if err != nil { + return err + } + + // Prepare the event data + arguments := abi.Arguments{event.Inputs[1], event.Inputs[2]} + packed, err := arguments.Pack(proposalID, options) + if err != nil { + return err + } + + stateDB.AddLog(ðtypes.Log{ + Address: p.Address(), + Topics: topics, + Data: packed, + BlockNumber: uint64(ctx.BlockHeight()), //nolint:gosec // G115 + }) + + return nil +} diff --git a/precompiles/gov/events_test.go b/precompiles/gov/events_test.go index ef6a3772dd..56f1afb1b4 100644 --- a/precompiles/gov/events_test.go +++ b/precompiles/gov/events_test.go @@ -84,3 +84,84 @@ func (s *PrecompileTestSuite) TestVoteEvent() { } } } + +func (s *PrecompileTestSuite) TestVoteWeightedEvent() { + var ( + stDB *statedb.StateDB + ctx sdk.Context + method = s.precompile.Methods[gov.VoteWeightedMethod] + ) + + testCases := []struct { + name string + malleate func(voter common.Address, proposalId uint64, options gov.WeightedVoteOptions) []interface{} + postCheck func() + gas uint64 + expError bool + errContains string + }{ + { + "success - the correct VoteWeighted event is emitted", + func(voter common.Address, proposalId uint64, options gov.WeightedVoteOptions) []interface{} { + return []interface{}{ + voter, + proposalId, + options, + "", + } + }, + func() { + log := stDB.Logs()[0] + s.Require().Equal(log.Address, s.precompile.Address()) + + // Check event signature matches the one emitted + event := s.precompile.ABI.Events[gov.EventTypeVoteWeighted] + s.Require().Equal(crypto.Keccak256Hash([]byte(event.Sig)), common.HexToHash(log.Topics[0].Hex())) + s.Require().Equal(log.BlockNumber, uint64(ctx.BlockHeight())) //nolint:gosec // G115 + + // Check the fully unpacked event matches the one emitted + var voteWeightedEvent gov.EventVoteWeighted + err := cmn.UnpackLog(s.precompile.ABI, &voteWeightedEvent, gov.EventTypeVoteWeighted, *log) + s.Require().NoError(err) + s.Require().Equal(s.keyring.GetAddr(0), voteWeightedEvent.Voter) + s.Require().Equal(uint64(1), voteWeightedEvent.ProposalId) + s.Require().Equal(2, len(voteWeightedEvent.Options)) + s.Require().Equal(uint8(1), voteWeightedEvent.Options[0].Option) + s.Require().Equal("0.70", voteWeightedEvent.Options[0].Weight) + s.Require().Equal(uint8(2), voteWeightedEvent.Options[1].Option) + s.Require().Equal("0.30", voteWeightedEvent.Options[1].Weight) + }, + 20000, + false, + "", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + stDB = s.network.GetStateDB() + ctx = s.network.GetContext() + + contract := vm.NewContract(vm.AccountRef(s.keyring.GetAddr(0)), s.precompile, big.NewInt(0), tc.gas) + ctx = ctx.WithGasMeter(storetypes.NewInfiniteGasMeter()) + initialGas := ctx.GasMeter().GasConsumed() + s.Require().Zero(initialGas) + + options := gov.WeightedVoteOptions{ + {Option: 1, Weight: "0.70"}, + {Option: 2, Weight: "0.30"}, + } + + _, err := s.precompile.VoteWeighted(ctx, s.keyring.GetAddr(0), contract, stDB, &method, tc.malleate(s.keyring.GetAddr(0), 1, options)) + + if tc.expError { + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.errContains) + } else { + s.Require().NoError(err) + tc.postCheck() + } + }) + } +} diff --git a/precompiles/gov/gov.go b/precompiles/gov/gov.go index a16051241b..214350dc6c 100644 --- a/precompiles/gov/gov.go +++ b/precompiles/gov/gov.go @@ -104,6 +104,8 @@ func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz [ // gov transactions case VoteMethod: bz, err = p.Vote(ctx, evm.Origin, contract, stateDB, method, args) + case VoteWeightedMethod: + bz, err = p.VoteWeighted(ctx, evm.Origin, contract, stateDB, method, args) // gov queries case GetVoteMethod: bz, err = p.GetVote(ctx, method, contract, args) @@ -134,9 +136,10 @@ func (p Precompile) Run(evm *vm.EVM, contract *vm.Contract, readOnly bool) (bz [ // // Available gov transactions are: // - Vote +// - VoteWeighted func (Precompile) IsTransaction(methodName string) bool { switch methodName { - case VoteMethod: + case VoteMethod, VoteWeightedMethod: return true default: return false diff --git a/precompiles/gov/integration_test.go b/precompiles/gov/integration_test.go index 88cd3a67be..21e5b5c646 100644 --- a/precompiles/gov/integration_test.go +++ b/precompiles/gov/integration_test.go @@ -131,6 +131,81 @@ var _ = Describe("Calling governance precompile from EOA", func() { }) }) + Describe("Execute VoteWeighted transaction", func() { + const method = gov.VoteWeightedMethod + + BeforeEach(func() { + callArgs.MethodName = method + }) + + It("should return error if the provided gasLimit is too low", func() { + txArgs.GasLimit = 30000 + callArgs.Args = []interface{}{ + s.keyring.GetAddr(0), + proposalID, + []gov.WeightedVoteOption{ + {Option: 1, Weight: "0.5"}, + {Option: 2, Weight: "0.5"}, + }, + metadata, + } + + _, _, err := s.factory.CallContractAndCheckLogs(s.keyring.GetPrivKey(0), txArgs, callArgs, outOfGasCheck) + Expect(err).To(BeNil()) + + // tally result should remain unchanged + proposal, _ := s.network.App.GovKeeper.Proposals.Get(s.network.GetContext(), proposalID) + _, _, tallyResult, err := s.network.App.GovKeeper.Tally(s.network.GetContext(), proposal) + Expect(err).To(BeNil()) + Expect(tallyResult.YesCount).To(Equal("0"), "expected tally result to remain unchanged") + }) + + It("should return error if the origin is different than the voter", func() { + callArgs.Args = []interface{}{ + differentAddr, + proposalID, + []gov.WeightedVoteOption{ + {Option: 1, Weight: "0.5"}, + {Option: 2, Weight: "0.5"}, + }, + metadata, + } + + voterSetCheck := defaultLogCheck.WithErrContains(gov.ErrDifferentOrigin, s.keyring.GetAddr(0).String(), differentAddr.String()) + + _, _, err := s.factory.CallContractAndCheckLogs(s.keyring.GetPrivKey(0), txArgs, callArgs, voterSetCheck) + Expect(err).To(BeNil()) + }) + + It("should vote weighted success", func() { + callArgs.Args = []interface{}{ + s.keyring.GetAddr(0), + proposalID, + []gov.WeightedVoteOption{ + {Option: 1, Weight: "0.7"}, + {Option: 2, Weight: "0.3"}, + }, + metadata, + } + + voterSetCheck := passCheck.WithExpEvents(gov.EventTypeVoteWeighted) + + _, _, err := s.factory.CallContractAndCheckLogs(s.keyring.GetPrivKey(0), txArgs, callArgs, voterSetCheck) + Expect(err).To(BeNil(), "error while calling the precompile") + + // tally result should be updated + proposal, _ := s.network.App.GovKeeper.Proposals.Get(s.network.GetContext(), proposalID) + _, _, tallyResult, err := s.network.App.GovKeeper.Tally(s.network.GetContext(), proposal) + Expect(err).To(BeNil()) + + expectedYesCount := math.NewInt(21e17) // 70% of 3e18 + Expect(tallyResult.YesCount).To(Equal(expectedYesCount.String()), "expected tally result yes count updated") + + expectedAbstainCount := math.NewInt(9e17) // 30% of 3e18 + Expect(tallyResult.AbstainCount).To(Equal(expectedAbstainCount.String()), "expected tally result no count updated") + }) + }) + // ===================================== // QUERIES // ===================================== @@ -179,6 +254,59 @@ var _ = Describe("Calling governance precompile from EOA", func() { }) }) + Context("weighted vote query", func() { + method := gov.GetVoteMethod + BeforeEach(func() { + // submit a weighted vote + voteArgs := factory.CallArgs{ + ContractABI: s.precompile.ABI, + MethodName: gov.VoteWeightedMethod, + Args: []interface{}{ + s.keyring.GetAddr(0), + proposalID, + []gov.WeightedVoteOption{ + {Option: 1, Weight: "0.7"}, + {Option: 2, Weight: "0.3"}, + }, + metadata, + }, + } + + voterSetCheck := passCheck.WithExpEvents(gov.EventTypeVoteWeighted) + + _, _, err := s.factory.CallContractAndCheckLogs(s.keyring.GetPrivKey(0), txArgs, voteArgs, voterSetCheck) + Expect(err).To(BeNil(), "error while calling the precompile") + Expect(s.network.NextBlock()).To(BeNil()) + }) + + It("should return a weighted vote", func() { + callArgs.MethodName = method + callArgs.Args = []interface{}{proposalID, s.keyring.GetAddr(0)} + txArgs.GasLimit = 200_000 + + _, ethRes, err := s.factory.CallContractAndCheckLogs( + s.keyring.GetPrivKey(0), + txArgs, + callArgs, + passCheck, + ) + Expect(err).To(BeNil(), "error while calling the smart contract: %v", err) + + var out gov.VoteOutput + err = s.precompile.UnpackIntoInterface(&out, method, ethRes.Ret) + Expect(err).To(BeNil()) + + Expect(out.Vote.Voter).To(Equal(s.keyring.GetAddr(0))) + Expect(out.Vote.ProposalId).To(Equal(proposalID)) + Expect(out.Vote.Metadata).To(Equal(metadata)) + Expect(out.Vote.Options).To(HaveLen(2)) + Expect(out.Vote.Options[0].Option).To(Equal(uint8(1))) + Expect(out.Vote.Options[0].Weight).To(Equal("0.7")) + Expect(out.Vote.Options[1].Option).To(Equal(uint8(2))) + Expect(out.Vote.Options[1].Weight).To(Equal("0.3")) + }) + }) + Context("votes query", func() { method := gov.GetVotesMethod BeforeEach(func() { diff --git a/precompiles/gov/tx.go b/precompiles/gov/tx.go index 47db0bf7bd..fbbd68bffc 100644 --- a/precompiles/gov/tx.go +++ b/precompiles/gov/tx.go @@ -18,6 +18,8 @@ import ( const ( // VoteMethod defines the ABI method name for the gov Vote transaction. VoteMethod = "vote" + // VoteWeightedMethod defines the ABI method name for the gov VoteWeighted transaction. + VoteWeightedMethod = "voteWeighted" ) // Vote claims the rewards accumulated by a delegator from multiple or all validators. @@ -52,3 +54,36 @@ func (p Precompile) Vote( return method.Outputs.Pack(true) } + +// VoteWeighted claims the rewards accumulated by a delegator from multiple or all validators. +func (p Precompile) VoteWeighted( + ctx sdk.Context, + origin common.Address, + contract *vm.Contract, + stateDB vm.StateDB, + method *abi.Method, + args []interface{}, +) ([]byte, error) { + msg, voterHexAddr, options, err := NewMsgVoteWeighted(method, args) + if err != nil { + return nil, err + } + + // If the contract is the voter, we don't need an origin check + // Otherwise check if the origin matches the delegator address + isContractVoter := contract.CallerAddress == voterHexAddr && contract.CallerAddress != origin + if !isContractVoter && origin != voterHexAddr { + return nil, fmt.Errorf(ErrDifferentOrigin, origin.String(), voterHexAddr.String()) + } + + msgSrv := govkeeper.NewMsgServerImpl(&p.govKeeper) + if _, err = msgSrv.VoteWeighted(ctx, msg); err != nil { + return nil, err + } + + if err = p.EmitVoteWeightedEvent(ctx, stateDB, voterHexAddr, msg.ProposalId, options); err != nil { + return nil, err + } + + return method.Outputs.Pack(true) +} diff --git a/precompiles/gov/tx_test.go b/precompiles/gov/tx_test.go index a0c9945787..c9732b15f4 100644 --- a/precompiles/gov/tx_test.go +++ b/precompiles/gov/tx_test.go @@ -142,3 +142,137 @@ func (s *PrecompileTestSuite) TestVote() { }) } } + +func (s *PrecompileTestSuite) TestVoteWeighted() { + var ctx sdk.Context + method := s.precompile.Methods[gov.VoteWeightedMethod] + newVoterAddr := utiltx.GenerateAddress() + const proposalID uint64 = 1 + const metadata = "metadata" + + testCases := []struct { + name string + malleate func() []interface{} + postCheck func() + gas uint64 + expError bool + errContains string + }{ + { + "fail - empty input args", + func() []interface{} { + return []interface{}{} + }, + func() {}, + 200000, + true, + fmt.Sprintf(cmn.ErrInvalidNumberOfArgs, 4, 0), + }, + { + "fail - invalid voter address", + func() []interface{} { + return []interface{}{ + "", + proposalID, + []gov.WeightedVoteOption{}, + metadata, + } + }, + func() {}, + 200000, + true, + "invalid voter address", + }, + { + "fail - using a different voter address", + func() []interface{} { + return []interface{}{ + newVoterAddr, + proposalID, + []gov.WeightedVoteOption{}, + metadata, + } + }, + func() {}, + 200000, + true, + "does not match the voter address", + }, + { + "fail - invalid vote option", + func() []interface{} { + return []interface{}{ + s.keyring.GetAddr(0), + proposalID, + []gov.WeightedVoteOption{{Option: 10, Weight: "1.0"}}, + metadata, + } + }, + func() {}, + 200000, + true, + "invalid vote option", + }, + { + "fail - invalid weight sum", + func() []interface{} { + return []interface{}{ + s.keyring.GetAddr(0), + proposalID, + []gov.WeightedVoteOption{ + {Option: 1, Weight: "0.5"}, + {Option: 2, Weight: "0.6"}, + }, + metadata, + } + }, + func() {}, + 200000, + true, + "total weight overflow 1.00", + }, + { + "success - vote weighted proposal", + func() []interface{} { + return []interface{}{ + s.keyring.GetAddr(0), + proposalID, + []gov.WeightedVoteOption{ + {Option: 1, Weight: "0.7"}, + {Option: 2, Weight: "0.3"}, + }, + metadata, + } + }, + func() { + proposal, _ := s.network.App.GovKeeper.Proposals.Get(ctx, proposalID) + _, _, tallyResult, err := s.network.App.GovKeeper.Tally(ctx, proposal) + s.Require().NoError(err) + s.Require().Equal("2100000000000000000", tallyResult.YesCount) + s.Require().Equal("900000000000000000", tallyResult.AbstainCount) + }, + 200000, + false, + "", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + s.SetupTest() + ctx = s.network.GetContext() + + var contract *vm.Contract + contract, ctx = testutil.NewPrecompileContract(s.T(), ctx, s.keyring.GetAddr(0), s.precompile, tc.gas) + + _, err := s.precompile.VoteWeighted(ctx, s.keyring.GetAddr(0), contract, s.network.GetStateDB(), &method, tc.malleate()) + + if tc.expError { + s.Require().ErrorContains(err, tc.errContains) + } else { + s.Require().NoError(err) + tc.postCheck() + } + }) + } +} diff --git a/precompiles/gov/types.go b/precompiles/gov/types.go index b82bb8d1f3..720f84d437 100644 --- a/precompiles/gov/types.go +++ b/precompiles/gov/types.go @@ -24,6 +24,13 @@ type EventVote struct { Option uint8 } +// EventVoteWeighted defines the event data for the VoteWeighted transaction. +type EventVoteWeighted struct { + Voter common.Address + ProposalId uint64 //nolint:revive,stylecheck + Options WeightedVoteOptions +} + // VotesInput defines the input for the Votes query. type VotesInput struct { ProposalId uint64 //nolint:revive,stylecheck @@ -55,6 +62,9 @@ type WeightedVoteOption struct { Weight string } +// WeightedVoteOptions defines a slice of WeightedVoteOption. +type WeightedVoteOptions []WeightedVoteOption + // NewMsgVote creates a new MsgVote instance. func NewMsgVote(args []interface{}) (*govv1.MsgVote, common.Address, error) { if len(args) != 4 { @@ -91,6 +101,52 @@ func NewMsgVote(args []interface{}) (*govv1.MsgVote, common.Address, error) { return msg, voterAddress, nil } +// NewMsgVoteWeighted creates a new MsgVoteWeighted instance. +func NewMsgVoteWeighted(method *abi.Method, args []interface{}) (*govv1.MsgVoteWeighted, common.Address, WeightedVoteOptions, error) { + if len(args) != 4 { + return nil, common.Address{}, WeightedVoteOptions{}, fmt.Errorf(cmn.ErrInvalidNumberOfArgs, 4, len(args)) + } + + voterAddress, ok := args[0].(common.Address) + if !ok || voterAddress == (common.Address{}) { + return nil, common.Address{}, WeightedVoteOptions{}, fmt.Errorf(ErrInvalidVoter, args[0]) + } + + proposalID, ok := args[1].(uint64) + if !ok { + return nil, common.Address{}, WeightedVoteOptions{}, fmt.Errorf(ErrInvalidProposalID, args[1]) + } + + // Unpack the input struct + var options WeightedVoteOptions + arguments := abi.Arguments{method.Inputs[2]} + if err := arguments.Copy(&options, []interface{}{args[2]}); err != nil { + return nil, common.Address{}, WeightedVoteOptions{}, fmt.Errorf("error while unpacking args to Options struct: %s", err) + } + + weightedOptions := make([]*govv1.WeightedVoteOption, len(options)) + for i, option := range options { + weightedOptions[i] = &govv1.WeightedVoteOption{ + Option: govv1.VoteOption(option.Option), + Weight: option.Weight, + } + } + + metadata, ok := args[3].(string) + if !ok { + return nil, common.Address{}, WeightedVoteOptions{}, fmt.Errorf(ErrInvalidMetadata, args[3]) + } + + msg := &govv1.MsgVoteWeighted{ + ProposalId: proposalID, + Voter: sdk.AccAddress(voterAddress.Bytes()).String(), + Options: weightedOptions, + Metadata: metadata, + } + + return msg, voterAddress, options, nil +} + // ParseVotesArgs parses the arguments for the Votes query. func ParseVotesArgs(method *abi.Method, args []interface{}) (*govv1.QueryVotesRequest, error) { if len(args) != 2 {