From ffc100695369c63b1e966cc982a687c135a16a0b Mon Sep 17 00:00:00 2001 From: Pascal Precht <445106+0x-r4bbit@users.noreply.github.com> Date: Thu, 4 May 2023 17:40:52 +0200 Subject: [PATCH] Add permission checks for ENS token permissions Also add tests for creating and editing token permissions as well as a test for checking ENS ownership. --- eth-node/bridge/geth/ens/ens.go | 12 +++ eth-node/types/ens/ens.go | 7 +- protocol/communities/manager.go | 42 ++++++++ protocol/communities_messenger_test.go | 138 +++++++++++++++++++++++++ protocol/ens/verifier.go | 6 ++ 5 files changed, 204 insertions(+), 1 deletion(-) diff --git a/eth-node/bridge/geth/ens/ens.go b/eth-node/bridge/geth/ens/ens.go index 69461d29a..f06db0eb5 100644 --- a/eth-node/bridge/geth/ens/ens.go +++ b/eth-node/bridge/geth/ens/ens.go @@ -11,6 +11,7 @@ import ( ens "github.com/wealdtech/go-ens/v3" "go.uber.org/zap" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/status-im/status-go/eth-node/crypto" @@ -30,6 +31,17 @@ func NewVerifier(logger *zap.Logger) *Verifier { return &Verifier{logger: logger} } +func (m *Verifier) ReverseResolve(address common.Address, rpcEndpoint string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), contractQueryTimeout) + defer cancel() + + ethClient, err := ethclient.DialContext(ctx, rpcEndpoint) + if err != nil { + return "", err + } + return ens.ReverseResolve(ethClient, address) +} + func (m *Verifier) verifyENSName(ensInfo enstypes.ENSDetails, ethclient *ethclient.Client) enstypes.ENSResponse { publicKeyStr := ensInfo.PublicKeyString ensName := ensInfo.Name diff --git a/eth-node/types/ens/ens.go b/eth-node/types/ens/ens.go index 4e0530d35..6424143aa 100644 --- a/eth-node/types/ens/ens.go +++ b/eth-node/types/ens/ens.go @@ -1,10 +1,15 @@ package enstypes -import "crypto/ecdsa" +import ( + "crypto/ecdsa" + + "github.com/ethereum/go-ethereum/common" +) type ENSVerifier interface { // CheckBatch verifies that a registered ENS name matches the expected public key CheckBatch(ensDetails []ENSDetails, rpcEndpoint, contractAddress string) (map[string]ENSResponse, error) + ReverseResolve(address common.Address, rpcEndpoint string) (string, error) } type ENSDetails struct { diff --git a/protocol/communities/manager.go b/protocol/communities/manager.go index e3368b8e2..2a05a0b32 100644 --- a/protocol/communities/manager.go +++ b/protocol/communities/manager.go @@ -1632,6 +1632,11 @@ func (m *Manager) checkPermissionToJoin(permissions []*protobuf.CommunityTokenPe return response, err } + ownedENSNames, err := m.getOwnedENS(walletAddresses) + if err != nil { + return response, err + } + for _, tokenPermission := range permissions { permissionRequirementsMet := true @@ -1685,6 +1690,25 @@ func (m *Manager) checkPermissionToJoin(permissions []*protobuf.CommunityTokenPe if ownedERC20Tokens[tokenRequirement.Symbol].Cmp(big.NewFloat(amount)) != -1 { tokenRequirementMet = true } + } else if tokenRequirement.Type == protobuf.CommunityTokenType_ENS { + if len(ownedENSNames) == 0 { + response.Permissions[tokenPermission.Id].Criteria = append(response.Permissions[tokenPermission.Id].Criteria, false) + continue + } + if !strings.HasPrefix(tokenRequirement.EnsPattern, "*.") { + for _, ownedENS := range ownedENSNames { + if ownedENS == tokenRequirement.EnsPattern { + tokenRequirementMet = true + } + } + } else { + parentName := tokenRequirement.EnsPattern[2:] + for _, ownedENS := range ownedENSNames { + if strings.HasSuffix(ownedENS, parentName) { + tokenRequirementMet = true + } + } + } } if !tokenRequirementMet { permissionRequirementsMet = false @@ -1815,6 +1839,24 @@ func (m *Manager) getAccumulatedTokenBalances(accounts []gethcommon.Address, tok return accumulatedBalances, nil } +func (m *Manager) getOwnedENS(addresses []gethcommon.Address) ([]string, error) { + ownedENS := make([]string, 0) + if m.ensVerifier == nil { + m.logger.Warn("no ensVerifier configured for communities manager") + return ownedENS, nil + } + for _, address := range addresses { + name, err := m.ensVerifier.ReverseResolve(address) + if err != nil && err.Error() != "not a resolver" { + return ownedENS, err + } + if name != "" { + ownedENS = append(ownedENS, name) + } + } + return ownedENS, nil +} + func (m *Manager) HandleCommunityRequestToJoinResponse(signer *ecdsa.PublicKey, request *protobuf.CommunityRequestToJoinResponse) (*RequestToJoin, error) { pkString := common.PubkeyToHex(&m.identity.PublicKey) diff --git a/protocol/communities_messenger_test.go b/protocol/communities_messenger_test.go index 87429056e..e904e302c 100644 --- a/protocol/communities_messenger_test.go +++ b/protocol/communities_messenger_test.go @@ -2211,6 +2211,144 @@ func (s *MessengerCommunitiesSuite) TestDeclineAccess() { s.Require().Len(requestsToJoin, 0) } +func (s *MessengerCommunitiesSuite) TestCreateTokenPermission() { + community := s.createCommunity() + + createTokenPermission := &requests.CreateCommunityTokenPermission{ + CommunityID: community.ID(), + Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ERC20, + ContractAddresses: map[uint64]string{uint64(1): "0x123"}, + Symbol: "TEST", + Amount: "100", + Decimals: uint64(18), + }, + }, + } + + response, err := s.admin.CreateCommunityTokenPermission(createTokenPermission) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.Communities(), 1) + + tokenPermissions := response.Communities()[0].TokenPermissions() + for _, tokenPermission := range tokenPermissions { + for _, tc := range tokenPermission.TokenCriteria { + s.Require().Equal(tc.Type, protobuf.CommunityTokenType_ERC20) + s.Require().Equal(tc.Symbol, "TEST") + s.Require().Equal(tc.Amount, "100") + s.Require().Equal(tc.Decimals, uint64(18)) + } + } +} + +func (s *MessengerCommunitiesSuite) TestEditTokenPermission() { + community := s.createCommunity() + + tokenPermission := &requests.CreateCommunityTokenPermission{ + CommunityID: community.ID(), + Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ERC20, + ContractAddresses: map[uint64]string{uint64(1): "0x123"}, + Symbol: "TEST", + Amount: "100", + Decimals: uint64(18), + }, + }, + } + + response, err := s.admin.CreateCommunityTokenPermission(tokenPermission) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.Communities(), 1) + + tokenPermissions := response.Communities()[0].TokenPermissions() + + var tokenPermissionID string + for id := range tokenPermissions { + tokenPermissionID = id + } + + tokenPermission.TokenCriteria[0].Symbol = "TESTUpdated" + tokenPermission.TokenCriteria[0].Amount = "200" + tokenPermission.TokenCriteria[0].Decimals = uint64(20) + + editTokenPermission := &requests.EditCommunityTokenPermission{ + PermissionID: tokenPermissionID, + CreateCommunityTokenPermission: *tokenPermission, + } + + response2, err := s.admin.EditCommunityTokenPermission(editTokenPermission) + s.Require().NoError(err) + // wait for `checkMemberPermissions` to finish + time.Sleep(1 * time.Second) + s.Require().NotNil(response2) + s.Require().Len(response2.Communities(), 1) + + tokenPermissions = response2.Communities()[0].TokenPermissions() + for _, tokenPermission := range tokenPermissions { + for _, tc := range tokenPermission.TokenCriteria { + s.Require().Equal(tc.Type, protobuf.CommunityTokenType_ERC20) + s.Require().Equal(tc.Symbol, "TESTUpdated") + s.Require().Equal(tc.Amount, "200") + s.Require().Equal(tc.Decimals, uint64(20)) + } + } +} + +func (s *MessengerCommunitiesSuite) TestRequestAccessWithENSTokenPermission() { + community := s.createCommunity() + + createTokenPermission := &requests.CreateCommunityTokenPermission{ + CommunityID: community.ID(), + Type: protobuf.CommunityTokenPermission_BECOME_MEMBER, + TokenCriteria: []*protobuf.TokenCriteria{ + &protobuf.TokenCriteria{ + Type: protobuf.CommunityTokenType_ENS, + EnsPattern: "test.stateofus.eth", + }, + }, + } + + response, err := s.admin.CreateCommunityTokenPermission(createTokenPermission) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.Communities(), 1) + + s.advertiseCommunityTo(community, s.alice) + + requestToJoin := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + // We try to join the org + response, err = s.alice.RequestToJoinCommunity(requestToJoin) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.RequestsToJoinCommunity, 1) + + requestToJoin1 := response.RequestsToJoinCommunity[0] + s.Require().Equal(communities.RequestToJoinStatePending, requestToJoin1.State) + + // Retrieve request to join + err = tt.RetryWithBackOff(func() error { + response, err = s.admin.RetrieveAll() + return err + }) + s.Require().NoError(err) + // We don't expect a requestToJoin in the response because due + // to missing revealed wallet addresses, the request should've + // been declined right away + s.Require().Len(response.RequestsToJoinCommunity, 0) + + // Ensure alice is not a member of the community + allCommunities, err := s.admin.Communities() + if bytes.Equal(allCommunities[0].ID(), community.ID()) { + s.Require().False(allCommunities[0].HasMember(&s.alice.identity.PublicKey)) + } +} + func (s *MessengerCommunitiesSuite) TestLeaveAndRejoinCommunity() { community := s.createCommunity() s.advertiseCommunityTo(community, s.alice) diff --git a/protocol/ens/verifier.go b/protocol/ens/verifier.go index 992467885..cb753e4f6 100644 --- a/protocol/ens/verifier.go +++ b/protocol/ens/verifier.go @@ -6,6 +6,7 @@ import ( "go.uber.org/zap" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/status-im/status-go/eth-node/types" enstypes "github.com/status-im/status-go/eth-node/types/ens" "github.com/status-im/status-go/protocol/common" @@ -129,6 +130,11 @@ func (v *Verifier) publish(records []*VerificationRecord) { } +func (v *Verifier) ReverseResolve(address gethcommon.Address) (string, error) { + verifier := v.node.NewENSVerifier(v.logger) + return verifier.ReverseResolve(address, v.rpcEndpoint) +} + // Verify verifies that a registered ENS name matches the expected public key func (v *Verifier) verify(rpcEndpoint, contractAddress string) error { v.logger.Debug("verifying ENS Names", zap.String("endpoint", rpcEndpoint))