diff --git a/runtime/common/orderedmap/orderedmap.go b/runtime/common/orderedmap/orderedmap.go index 462b06ca77..1f584ff886 100644 --- a/runtime/common/orderedmap/orderedmap.go +++ b/runtime/common/orderedmap/orderedmap.go @@ -150,6 +150,9 @@ func (om *OrderedMap[K, V]) Delete(key K) (oldValue V, present bool) { // Len returns the length of the ordered map. func (om *OrderedMap[K, V]) Len() int { + if om == nil { + return 0 + } return len(om.pairs) } diff --git a/runtime/sema/entitlementset.go b/runtime/sema/entitlementset.go index 986098f786..964c86df4c 100644 --- a/runtime/sema/entitlementset.go +++ b/runtime/sema/entitlementset.go @@ -48,12 +48,16 @@ func disjunctionKey(disjunction *EntitlementOrderedSet) string { type DisjunctionOrderedSet = orderedmap.OrderedMap[string, *EntitlementOrderedSet] // EntitlementSet is a set (conjunction) of entitlements and entitlement disjunctions. -// e.g. {entitlements: A, B; disjunctions: (C | D), (E | F)} +// e.g. {entitlements: A, B; disjunctions: (C ∨ D), (E ∨ F)} +// This is distinct from an EntitlementSetAccess in Cadence, which is a program-level +// (and possibly non-minimal) approximation of this abstract set type EntitlementSet struct { // Entitlements is a set of entitlements Entitlements *EntitlementOrderedSet // Disjunctions is a set of entitlement disjunctions, keyed by disjunctionKey Disjunctions *DisjunctionOrderedSet + // Minimized tracks whether the set is minimized or not + minimized bool } // Add adds an entitlement to the set. @@ -67,6 +71,8 @@ func (s *EntitlementSet) Add(entitlementType *EntitlementType) { s.Entitlements = orderedmap.New[EntitlementOrderedSet](1) } s.Entitlements.Set(entitlementType, struct{}{}) + + s.minimized = false } // AddDisjunction adds an entitlement disjunction to the set. @@ -92,6 +98,8 @@ func (s *EntitlementSet) AddDisjunction(disjunction *EntitlementOrderedSet) { s.Disjunctions = orderedmap.New[DisjunctionOrderedSet](1) } s.Disjunctions.Set(key, disjunction) + + s.minimized = false } // Merge merges the other entitlement set into this set. @@ -116,12 +124,16 @@ func (s *EntitlementSet) Merge(other *EntitlementSet) { s.AddDisjunction(disjunction) }) } + + s.minimized = false } // Minimize minimizes the entitlement set. // It removes disjunctions that contain entitlements // which are also in the entitlement set func (s *EntitlementSet) Minimize() { + defer func() { s.minimized = true }() + // If there are no entitlements or no disjunctions, // there is nothing to minimize if s.Entitlements == nil || s.Disjunctions == nil { @@ -141,14 +153,38 @@ func (s *EntitlementSet) Minimize() { } } +// Returns whether this entitlement set is minimally representable in Cadence. +// +// If true, this set can be exactly represented as a non-nested logical formula: i.e. either a single conjunction or a single disjunction +// If false, this set cannot be represented without nesting connective operators, and thus must be over-approximated when being +// represented in Cadence. +// As Cadence does not support nesting disjunctions and conjunctions in the same entitlement set, this function returns false +// when s.Entitlements and s.Disjunctions are both non-empty, or when s.Disjunctions has more than one element +func (s *EntitlementSet) IsMinimallyRepresentable() bool { + if s == nil { + return true + } + + if !s.minimized { + s.Minimize() + } + + return s.Disjunctions.Len() == 0 || (s.Entitlements.Len() == 0 && s.Disjunctions.Len() == 1) +} + // Access returns the access represented by the entitlement set. // The set is minimized before the access is computed. +// Note that this function may over-approximate the permissions +// required to represent this set of entitlements as an access modifier that Cadence can use, +// e.g. `(A ∨ B) ∧ C` cannot be represented in Cadence and will the produce an over-approximation of `(A, B, C)` func (s *EntitlementSet) Access() Access { if s == nil { return UnauthorizedAccess } - s.Minimize() + if !s.minimized { + s.Minimize() + } var entitlements *EntitlementOrderedSet if s.Entitlements != nil && s.Entitlements.Len() > 0 { diff --git a/runtime/sema/entitlementset_test.go b/runtime/sema/entitlementset_test.go index ec5d0d80ad..19692a0ff9 100644 --- a/runtime/sema/entitlementset_test.go +++ b/runtime/sema/entitlementset_test.go @@ -40,6 +40,7 @@ func TestEntitlementSet_Add(t *testing.T) { } set.Add(e1) + assert.False(t, set.minimized) assert.Equal(t, 1, set.Entitlements.Len()) assert.Nil(t, set.Disjunctions) @@ -48,6 +49,7 @@ func TestEntitlementSet_Add(t *testing.T) { } set.Add(e2) + assert.False(t, set.minimized) assert.Equal(t, 2, set.Entitlements.Len()) assert.Nil(t, set.Disjunctions) }) @@ -70,6 +72,7 @@ func TestEntitlementSet_Add(t *testing.T) { set.AddDisjunction(e1e2) + assert.False(t, set.minimized) assert.Nil(t, set.Entitlements) assert.Equal(t, 1, set.Disjunctions.Len()) @@ -77,6 +80,7 @@ func TestEntitlementSet_Add(t *testing.T) { set.Add(e2) + assert.False(t, set.minimized) assert.Equal(t, 1, set.Entitlements.Len()) // NOTE: the set is not minimal, // the disjunction is not discarded @@ -108,6 +112,7 @@ func TestEntitlementSet_AddDisjunction(t *testing.T) { set.AddDisjunction(e1e2) + assert.False(t, set.minimized) assert.Nil(t, set.Entitlements) assert.Equal(t, 1, set.Disjunctions.Len()) @@ -115,6 +120,7 @@ func TestEntitlementSet_AddDisjunction(t *testing.T) { set.AddDisjunction(e1e2) + assert.False(t, set.minimized) assert.Nil(t, set.Entitlements) assert.Equal(t, 1, set.Disjunctions.Len()) @@ -126,6 +132,7 @@ func TestEntitlementSet_AddDisjunction(t *testing.T) { set.AddDisjunction(e2e1) + assert.False(t, set.minimized) assert.Nil(t, set.Entitlements) assert.Equal(t, 1, set.Disjunctions.Len()) @@ -141,6 +148,7 @@ func TestEntitlementSet_AddDisjunction(t *testing.T) { set.AddDisjunction(e2e3) + assert.False(t, set.minimized) assert.Nil(t, set.Entitlements) assert.Equal(t, 2, set.Disjunctions.Len()) }) @@ -156,6 +164,7 @@ func TestEntitlementSet_AddDisjunction(t *testing.T) { set.Add(e1) + assert.False(t, set.minimized) assert.Equal(t, 1, set.Entitlements.Len()) assert.Nil(t, set.Disjunctions) @@ -171,6 +180,7 @@ func TestEntitlementSet_AddDisjunction(t *testing.T) { set.AddDisjunction(e1e2) + assert.False(t, set.minimized) assert.Equal(t, 1, set.Entitlements.Len()) assert.Nil(t, set.Disjunctions) }) @@ -206,6 +216,7 @@ func TestEntitlementSet_Merge(t *testing.T) { set1.Add(e1) set1.AddDisjunction(e2e3) + assert.False(t, set1.minimized) assert.Equal(t, 1, set1.Entitlements.Len()) assert.Equal(t, 1, set1.Disjunctions.Len()) @@ -215,6 +226,7 @@ func TestEntitlementSet_Merge(t *testing.T) { set2.Add(e2) set2.AddDisjunction(e3e4) + assert.False(t, set2.minimized) assert.Equal(t, 1, set2.Entitlements.Len()) assert.Equal(t, 1, set2.Disjunctions.Len()) @@ -222,6 +234,7 @@ func TestEntitlementSet_Merge(t *testing.T) { set1.Merge(set2) + assert.False(t, set1.minimized) assert.Equal(t, 2, set1.Entitlements.Len()) assert.True(t, set1.Entitlements.Contains(e1)) assert.True(t, set1.Entitlements.Contains(e2)) @@ -257,6 +270,7 @@ func TestEntitlementSet_Minimize(t *testing.T) { set.Add(e1) // NOTE: the set is not minimal + assert.False(t, set.minimized) assert.Equal(t, 1, set.Entitlements.Len()) assert.Equal(t, 1, set.Disjunctions.Len()) @@ -264,24 +278,191 @@ func TestEntitlementSet_Minimize(t *testing.T) { set.Minimize() + assert.True(t, set.minimized) assert.Equal(t, 1, set.Entitlements.Len()) assert.Equal(t, 0, set.Disjunctions.Len()) } +func TestEntitlementSet_MinimallyRepresentable(t *testing.T) { + t.Parallel() + + t.Run("no entitlements, no disjunctions: {} = true", func(t *testing.T) { + t.Parallel() + + set := &EntitlementSet{} + assert.True(t, set.IsMinimallyRepresentable()) + }) + + t.Run("one entitlement, no disjunctions: {E1} = true", func(t *testing.T) { + t.Parallel() + + set := &EntitlementSet{} + + e1 := &EntitlementType{ + Identifier: "E1", + } + set.Add(e1) + + assert.True(t, set.IsMinimallyRepresentable()) + }) + + t.Run("two entitlements, no disjunctions: {E1, E2} = true", func(t *testing.T) { + t.Parallel() + + set := &EntitlementSet{} + + e1 := &EntitlementType{ + Identifier: "E1", + } + set.Add(e1) + + e2 := &EntitlementType{ + Identifier: "E2", + } + set.Add(e2) + + assert.True(t, set.IsMinimallyRepresentable()) + }) + + t.Run("one entitlement, redundant disjunction: {E1, (E1 | E2)} = true", func(t *testing.T) { + t.Parallel() + + set := &EntitlementSet{} + + e1 := &EntitlementType{ + Identifier: "E1", + } + + e2 := &EntitlementType{ + Identifier: "E2", + } + + set.Add(e1) + + e1e2 := orderedmap.New[EntitlementOrderedSet](2) + e1e2.Set(e1, struct{}{}) + e1e2.Set(e2, struct{}{}) + + set.AddDisjunction(e1e2) + + assert.True(t, set.IsMinimallyRepresentable()) + }) + + t.Run("two entitlements, two redundant disjunctions: {E1, E3, (E1 | E2), (E3 | E4)} = true", func(t *testing.T) { + t.Parallel() + + set := &EntitlementSet{} + + e1 := &EntitlementType{ + Identifier: "E1", + } + + e2 := &EntitlementType{ + Identifier: "E2", + } + + e3 := &EntitlementType{ + Identifier: "E1", + } + + e4 := &EntitlementType{ + Identifier: "E2", + } + + set.Add(e1) + set.Add(e3) + + e1e2 := orderedmap.New[EntitlementOrderedSet](2) + e1e2.Set(e1, struct{}{}) + e1e2.Set(e2, struct{}{}) + + set.AddDisjunction(e1e2) + + e3e4 := orderedmap.New[EntitlementOrderedSet](2) + e3e4.Set(e3, struct{}{}) + e3e4.Set(e4, struct{}{}) + + set.AddDisjunction(e3e4) + + assert.True(t, set.IsMinimallyRepresentable()) + }) + + t.Run("one entitlement, non-redundant disjunction: {E1, (E3 | E2)} = false", func(t *testing.T) { + t.Parallel() + + set := &EntitlementSet{} + + e1 := &EntitlementType{ + Identifier: "E1", + } + + e2 := &EntitlementType{ + Identifier: "E2", + } + + e3 := &EntitlementType{ + Identifier: "E3", + } + + set.Add(e1) + + e3e2 := orderedmap.New[EntitlementOrderedSet](2) + e3e2.Set(e3, struct{}{}) + e3e2.Set(e2, struct{}{}) + + set.AddDisjunction(e3e2) + + assert.False(t, set.IsMinimallyRepresentable()) + }) + + t.Run("two disjunctions: {(E1 | E2), (E2 | E3)} = false", func(t *testing.T) { + t.Parallel() + + set := &EntitlementSet{} + + e1 := &EntitlementType{ + Identifier: "E1", + } + + e2 := &EntitlementType{ + Identifier: "E2", + } + + e3 := &EntitlementType{ + Identifier: "E3", + } + + e1e2 := orderedmap.New[EntitlementOrderedSet](2) + e1e2.Set(e1, struct{}{}) + e1e2.Set(e2, struct{}{}) + + set.AddDisjunction(e1e2) + + e3e2 := orderedmap.New[EntitlementOrderedSet](2) + e3e2.Set(e3, struct{}{}) + e3e2.Set(e2, struct{}{}) + + set.AddDisjunction(e3e2) + + assert.False(t, set.IsMinimallyRepresentable()) + }) +} + func TestEntitlementSet_Access(t *testing.T) { t.Parallel() - t.Run("no entitlements, no disjunctions", func(t *testing.T) { + t.Run("no entitlements, no disjunctions: {} = unauthorized", func(t *testing.T) { t.Parallel() set := &EntitlementSet{} access := set.Access() + assert.True(t, set.minimized) assert.Equal(t, UnauthorizedAccess, access) }) - t.Run("entitlements, no disjunctions", func(t *testing.T) { + t.Run("entitlements, no disjunctions: {E1, E2} = auth(E1, E2)", func(t *testing.T) { t.Parallel() set := &EntitlementSet{} @@ -296,7 +477,9 @@ func TestEntitlementSet_Access(t *testing.T) { } set.Add(e2) + assert.False(t, set.minimized) access := set.Access() + assert.True(t, set.minimized) expectedEntitlements := orderedmap.New[EntitlementOrderedSet](2) expectedEntitlements.Set(e1, struct{}{}) @@ -311,7 +494,7 @@ func TestEntitlementSet_Access(t *testing.T) { ) }) - t.Run("no entitlements, one disjunction", func(t *testing.T) { + t.Run("no entitlements, one disjunction: {(E1 | E2)} = auth(E1 | E2)", func(t *testing.T) { t.Parallel() set := &EntitlementSet{} @@ -329,7 +512,9 @@ func TestEntitlementSet_Access(t *testing.T) { set.AddDisjunction(e1e2) + assert.False(t, set.minimized) access := set.Access() + assert.True(t, set.minimized) assert.Equal(t, EntitlementSetAccess{ @@ -340,7 +525,7 @@ func TestEntitlementSet_Access(t *testing.T) { ) }) - t.Run("no entitlements, two disjunctions", func(t *testing.T) { + t.Run("no entitlements, two disjunctions: {(E1 | E2), (E2 | E3)} = auth(E1, E2, E3)", func(t *testing.T) { t.Parallel() set := &EntitlementSet{} @@ -364,12 +549,20 @@ func TestEntitlementSet_Access(t *testing.T) { e2e3.Set(e3, struct{}{}) set.AddDisjunction(e1e2) + + assert.False(t, set.minimized) + set.Minimize() + assert.True(t, set.minimized) + set.AddDisjunction(e2e3) + assert.False(t, set.minimized) access := set.Access() + assert.True(t, set.minimized) // Cannot express (E1 | E2), (E2 | E3) in an access/auth, // so the result is the conjunction of all entitlements + assert.False(t, set.IsMinimallyRepresentable()) expectedEntitlements := orderedmap.New[EntitlementOrderedSet](3) expectedEntitlements.Set(e1, struct{}{}) @@ -385,7 +578,7 @@ func TestEntitlementSet_Access(t *testing.T) { ) }) - t.Run("entitlement, one disjunction, minimal", func(t *testing.T) { + t.Run("entitlement, one disjunction, not minimal: {E1, (E2 | E3)} = auth(E1, E2, E3)", func(t *testing.T) { t.Parallel() set := &EntitlementSet{} @@ -407,11 +600,14 @@ func TestEntitlementSet_Access(t *testing.T) { e2e3.Set(e3, struct{}{}) set.AddDisjunction(e2e3) + assert.False(t, set.minimized) access := set.Access() + assert.True(t, set.minimized) // Cannot express E1, (E2 | E3) in an access/auth, // so the result is the conjunction of all entitlements + assert.False(t, set.IsMinimallyRepresentable()) expectedEntitlements := orderedmap.New[EntitlementOrderedSet](3) expectedEntitlements.Set(e1, struct{}{}) @@ -427,7 +623,7 @@ func TestEntitlementSet_Access(t *testing.T) { ) }) - t.Run("entitlement, one disjunction, not minimal", func(t *testing.T) { + t.Run("entitlement, one disjunction, minimal: {(E1 | E2), E1} = auth(E1)", func(t *testing.T) { t.Parallel() set := &EntitlementSet{} @@ -444,12 +640,19 @@ func TestEntitlementSet_Access(t *testing.T) { e1e2.Set(e2, struct{}{}) set.AddDisjunction(e1e2) + assert.False(t, set.minimized) + + set.Minimize() + assert.True(t, set.minimized) set.Add(e1) + assert.False(t, set.minimized) access := set.Access() + assert.True(t, set.minimized) // NOTE: disjunction got removed during minimization + assert.True(t, set.IsMinimallyRepresentable()) expectedEntitlements := orderedmap.New[EntitlementOrderedSet](1) expectedEntitlements.Set(e1, struct{}{}) diff --git a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go index 0ba7d36662..5a2f0ba892 100644 --- a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go +++ b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validation_test.go @@ -19,6 +19,7 @@ package stdlib_test import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -196,6 +197,14 @@ func assertMissingDeclarationError(t *testing.T, err error, declName string) boo return assert.Equal(t, declName, missingDeclError.Name) } +func assertInvalidEntitlementsUpgradeError(t *testing.T, err error, declName string, accessString string) { + var invalidEntitlements *stdlib.UnrepresentableEntitlementsUpgrade + require.ErrorAs(t, err, &invalidEntitlements) + + require.Equal(t, declName, invalidEntitlements.Type.QualifiedString()) + require.Equal(t, accessString, invalidEntitlements.InvalidAuthorization.QualifiedString()) +} + func getContractUpdateError(t *testing.T, err error, contractName string) *stdlib.ContractUpdateError { require.Error(t, err) @@ -2613,3 +2622,169 @@ func TestEnumUpdates(t *testing.T) { require.NoError(t, err) }) } + +func TestContractUpgradeIsRepresentable(t *testing.T) { + + t.Parallel() + + test := func(isInterface bool) { + nameString := "composite" + if isInterface { + nameString = "interface" + } + + codeString := "" + if isInterface { + codeString = "interface" + } + + functionImplString := "{}" + if isInterface { + functionImplString = "" + } + + t.Run(fmt.Sprintf("grant one entitlement %s", nameString), func(t *testing.T) { + + t.Parallel() + + var oldCode = fmt.Sprintf(` + access(all) contract %[1]s Test { + access(all) resource %[1]s R { + access(all) fun a() %[2]s + } + } + `, codeString, functionImplString) + + var newCode = fmt.Sprintf(` + access(all) contract %[1]s Test { + access(all) entitlement E + access(all) resource %[1]s R { + access(E) fun a() %[2]s + } + } + `, codeString, functionImplString) + + err := testContractUpdate(t, oldCode, newCode) + require.NoError(t, err) + }) + + t.Run(fmt.Sprintf("grant two entitlements %s", nameString), func(t *testing.T) { + + t.Parallel() + + var oldCode = fmt.Sprintf(` + access(all) contract %[1]s Test { + access(all) resource %[1]s R { + access(all) fun a() %[2]s + access(all) fun b() %[2]s + } + } + `, codeString, functionImplString) + + var newCode = fmt.Sprintf(` + access(all) contract %[1]s Test { + access(all) entitlement E + access(all) entitlement F + access(all) resource %[1]s R { + access(E) fun a() %[2]s + access(F) fun b() %[2]s + } + } + `, codeString, functionImplString) + + err := testContractUpdate(t, oldCode, newCode) + require.NoError(t, err) + }) + + t.Run(fmt.Sprintf("redundant disjunction %s", nameString), func(t *testing.T) { + + t.Parallel() + + var oldCode = fmt.Sprintf(` + access(all) contract %[1]s Test { + access(all) resource %[1]s R { + access(all) fun a() %[2]s + access(all) fun b() %[2]s + } + } + `, codeString, functionImplString) + + var newCode = fmt.Sprintf(` + access(all) contract %[1]s Test { + access(all) entitlement E + access(all) entitlement F + access(all) resource %[1]s R { + access(E) fun a() %[2]s + access(E | F) fun b() %[2]s + } + } + `, codeString, functionImplString) + + err := testContractUpdate(t, oldCode, newCode) + require.NoError(t, err) + }) + + t.Run(fmt.Sprintf("non-redundant disjunction %s", nameString), func(t *testing.T) { + + t.Parallel() + + var oldCode = fmt.Sprintf(` + access(all) contract %[1]s Test { + access(all) resource %[1]s R { + access(all) fun a() %[2]s + access(all) fun b() %[2]s + } + } + `, codeString, functionImplString) + + var newCode = fmt.Sprintf(` + access(all) contract %[1]s Test { + access(all) entitlement E + access(all) entitlement F + access(all) entitlement G + access(all) resource %[1]s R { + access(E) fun a() %[2]s + access(F | G) fun b() %[2]s + } + } + `, codeString, functionImplString) + + err := testContractUpdate(t, oldCode, newCode) + cause := getSingleContractUpdateErrorCause(t, err, "Test") + assertInvalidEntitlementsUpgradeError(t, cause, "Test.R", "Test.E, Test.F, Test.G") + }) + + t.Run(fmt.Sprintf("two disjunctions %s", nameString), func(t *testing.T) { + + t.Parallel() + + var oldCode = fmt.Sprintf(` + access(all) contract %[1]s Test { + access(all) resource %[1]s R { + access(all) fun a() %[2]s + access(all) fun b() %[2]s + } + } + `, codeString, functionImplString) + + var newCode = fmt.Sprintf(` + access(all) contract %[1]s Test { + access(all) entitlement E + access(all) entitlement F + access(all) entitlement G + access(all) resource %[1]s R { + access(E | F) fun a() %[2]s + access(F | G) fun b() %[2]s + } + } + `, codeString, functionImplString) + + err := testContractUpdate(t, oldCode, newCode) + cause := getSingleContractUpdateErrorCause(t, err, "Test") + assertInvalidEntitlementsUpgradeError(t, cause, "Test.R", "Test.E, Test.F, Test.G") + }) + } + + test(true) + test(false) +} diff --git a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go index 2ff6e8a4f4..7cc5870309 100644 --- a/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go +++ b/runtime/stdlib/cadence_v0.42_to_v1_contract_upgrade_validator.go @@ -114,6 +114,16 @@ func (validator *CadenceV042ToV1ContractUpdateValidator) Validate() error { validator.checkConformanceV1, ) + // Check entitlements added to nested decls are all representable + nestedComposites := newRootDecl.DeclarationMembers().Composites() + for _, nestedComposite := range nestedComposites { + validator.validateEntitlementsRepresentableComposite(nestedComposite) + } + nestedInterfaces := newRootDecl.DeclarationMembers().Interfaces() + for _, nestedInterface := range nestedInterfaces { + validator.validateEntitlementsRepresentableInterface(nestedInterface) + } + if underlyingValidator.hasErrors() { return underlyingValidator.getContractUpdateError() } @@ -284,6 +294,34 @@ func (validator *CadenceV042ToV1ContractUpdateValidator) expectedAuthorizationOf return intersectionType.SupportedEntitlements().Access() } +func (validator *CadenceV042ToV1ContractUpdateValidator) validateEntitlementsRepresentableComposite(decl *ast.CompositeDeclaration) { + dummyNominalType := ast.NewNominalType(nil, decl.Identifier, nil) + compositeType := validator.getCompositeType(dummyNominalType) + supportedEntitlements := compositeType.SupportedEntitlements() + + if !supportedEntitlements.IsMinimallyRepresentable() { + validator.report(&UnrepresentableEntitlementsUpgrade{ + Type: compositeType, + InvalidAuthorization: supportedEntitlements.Access(), + Range: decl.Range, + }) + } +} + +func (validator *CadenceV042ToV1ContractUpdateValidator) validateEntitlementsRepresentableInterface(decl *ast.InterfaceDeclaration) { + dummyNominalType := ast.NewNominalType(nil, decl.Identifier, nil) + interfaceType := validator.getInterfaceType(dummyNominalType) + supportedEntitlements := interfaceType.SupportedEntitlements() + + if !supportedEntitlements.IsMinimallyRepresentable() { + validator.report(&UnrepresentableEntitlementsUpgrade{ + Type: interfaceType, + InvalidAuthorization: supportedEntitlements.Access(), + Range: decl.Range, + }) + } +} + func (validator *CadenceV042ToV1ContractUpdateValidator) checkEntitlementsUpgrade(newType *ast.ReferenceType) error { newAuthorization := newType.Authorization newEntitlementSet, isEntitlementsSet := newAuthorization.(ast.EntitlementSet) @@ -853,3 +891,29 @@ func (e *AuthorizationMismatchError) Error() string { e.FoundAuthorization.QualifiedString(), ) } + +// UnrepresentableEntitlementsUpgrade is reported during a contract upgrade, +// when a composite or interface type is given access modifiers on its field that would +// cause the migration to produce an unrepresentable entitlement set for references to that type +type UnrepresentableEntitlementsUpgrade struct { + Type sema.Type + InvalidAuthorization sema.Access + ast.Range +} + +var _ errors.UserError = &UnrepresentableEntitlementsUpgrade{} +var _ errors.SecondaryError = &UnrepresentableEntitlementsUpgrade{} + +func (*UnrepresentableEntitlementsUpgrade) IsUserError() {} + +func (e *UnrepresentableEntitlementsUpgrade) Error() string { + return fmt.Sprintf( + "unsafe access modifiers on %s: the entitlements migration would grant references to this type %s authorization, which is too permissive.", + e.Type.QualifiedString(), + e.InvalidAuthorization.QualifiedString(), + ) +} + +func (e *UnrepresentableEntitlementsUpgrade) SecondaryError() string { + return "Consider removing any disjunction access modifiers" +}