From e2267bc9ac2ab258444f91f95c62778d3378e50a Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Tue, 11 Jun 2024 12:47:14 -0700 Subject: [PATCH 1/4] Import contracts as references --- runtime/import_test.go | 104 ++++++++++++++++++++++ runtime/interpreter/interpreter_import.go | 25 ++++++ runtime/sema/check_import_declaration.go | 15 +++- runtime/tests/checker/import_test.go | 55 ++++++++++++ 4 files changed, 198 insertions(+), 1 deletion(-) diff --git a/runtime/import_test.go b/runtime/import_test.go index 077d6ae56e..79e01a4a55 100644 --- a/runtime/import_test.go +++ b/runtime/import_test.go @@ -406,3 +406,107 @@ func TestRuntimeCheckCyclicImportToSelfDuringDeploy(t *testing.T) { errs := checker.RequireCheckerErrors(t, checkerErr, 1) require.IsType(t, &sema.CyclicImportsError{}, errs[0]) } + +func TestRuntimeContractImport(t *testing.T) { + + t.Parallel() + + addressValue := Address{ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + } + + runtime := NewTestInterpreterRuntime() + + contract := []byte(` + access(all) contract Foo { + access(all) let x: [Int] + + access(all) fun answer(): Int { + return 42 + } + + access(all) struct Bar { + } + + init() { + self.x = [] + } + }`, + ) + + deploy := DeploymentTransaction("Foo", contract) + + script := []byte(` + import Foo from 0x01 + + access(all) fun main() { + var x: &[Int] = Foo.x + + var bar: Foo.Bar = Foo.Bar() + } + `) + + accountCodes := map[Location][]byte{} + var events []cadence.Event + + runtimeInterface := &TestRuntimeInterface{ + OnGetCode: func(location Location) (bytes []byte, err error) { + return accountCodes[location], nil + }, + Storage: NewTestLedger(nil, nil), + OnGetSigningAccounts: func() ([]Address, error) { + return []Address{addressValue}, nil + }, + OnResolveLocation: NewSingleIdentifierLocationResolver(t), + OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { + return accountCodes[location], nil + }, + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, + OnCreateAccount: func(payer Address) (address Address, err error) { + return addressValue, nil + }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + } + + nextTransactionLocation := NewTransactionLocationGenerator() + + err := runtime.ExecuteTransaction( + Script{ + Source: deploy, + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(t, err) + + nextScriptLocation := NewScriptLocationGenerator() + + _, err = runtime.ExecuteScript( + Script{ + Source: script, + }, + Context{ + Interface: runtimeInterface, + Location: nextScriptLocation(), + }, + ) + require.NoError(t, err) + + // Script + // + //var checkerErr *sema.CheckerError + //require.ErrorAs(t, err, &checkerErr) + // + //errs := checker.RequireCheckerErrors(t, checkerErr, 1) + // + //var importedProgramErr *sema.ImportedProgramError + //require.ErrorAs(t, errs[0], &importedProgramErr) +} diff --git a/runtime/interpreter/interpreter_import.go b/runtime/interpreter/interpreter_import.go index 62323435d4..3a6703fe85 100644 --- a/runtime/interpreter/interpreter_import.go +++ b/runtime/interpreter/interpreter_import.go @@ -23,6 +23,7 @@ import ( "time" "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" "github.com/onflow/cadence/runtime/sema" ) @@ -84,6 +85,30 @@ func (interpreter *Interpreter) importResolvedLocation(resolvedLocation sema.Res for _, name := range names { variable := variables[name] + value := variable.GetValue(interpreter) + + // If the variable is a contract value, then import it as a reference. + // This must be done at the type of importing, rather than when declaring the contract value. + compositeValue, ok := value.(*CompositeValue) + if ok && compositeValue.Kind == common.CompositeKindContract { + staticType := compositeValue.StaticType(interpreter) + semaType, err := interpreter.ConvertStaticToSemaType(staticType) + if err != nil { + panic(err) + } + + contractReference := NewEphemeralReferenceValue( + interpreter, + UnauthorizedAccess, + compositeValue, + semaType, + LocationRange{ + Location: interpreter.Location, + }, + ) + variable = NewVariableWithValue(interpreter, contractReference) + } + interpreter.setVariable(name, variable) interpreter.Globals.Set(name, variable) } diff --git a/runtime/sema/check_import_declaration.go b/runtime/sema/check_import_declaration.go index 3e323a9d67..5a3df7bc5e 100644 --- a/runtime/sema/check_import_declaration.go +++ b/runtime/sema/check_import_declaration.go @@ -178,6 +178,7 @@ func (checker *Checker) importResolvedLocation(resolvedLocation ResolvedLocation checker.valueActivations, resolvedLocation.Identifiers, allValueElements, + true, ) // Attempt to import the requested type declarations @@ -187,6 +188,7 @@ func (checker *Checker) importResolvedLocation(resolvedLocation ResolvedLocation checker.typeActivations, resolvedLocation.Identifiers, allTypeElements, + false, ) // For each identifier, report if the import is invalid due to @@ -300,6 +302,7 @@ func (checker *Checker) importElements( valueActivations *VariableActivations, requestedIdentifiers []ast.Identifier, availableElements *StringImportElementOrderedMap, + importValues bool, ) ( found map[ast.Identifier]bool, invalidAccessed map[ast.Identifier]ImportElement, @@ -351,9 +354,19 @@ func (checker *Checker) importElements( } } + elementType := element.Type + + if importValues { + // Imported contract values must be imported as a reference. + compositeType, ok := elementType.(*CompositeType) + if ok && compositeType.Kind == common.CompositeKindContract { + elementType = NewReferenceType(checker.memoryGauge, UnauthorizedAccess, compositeType) + } + } + _, err := valueActivations.declare(variableDeclaration{ identifier: name, - ty: element.Type, + ty: elementType, // TODO: implies that type is "re-exported" access: access, kind: element.DeclarationKind, diff --git a/runtime/tests/checker/import_test.go b/runtime/tests/checker/import_test.go index c98c3bec1c..9d8d87f601 100644 --- a/runtime/tests/checker/import_test.go +++ b/runtime/tests/checker/import_test.go @@ -716,3 +716,58 @@ func TestCheckImportVirtual(t *testing.T) { require.NoError(t, err) } + +func TestCheckImportContract(t *testing.T) { + + t.Parallel() + + importedChecker, err := ParseAndCheckWithOptions(t, + ` + access(all) contract Foo { + access(all) let x: [Int] + // access(all) let y: [Int] + + access(all) fun answer(): Int { + return 42 + } + + access(all) struct Bar { + } + + init() { + self.x = [] + // self.y = [] + } + }`, + ParseAndCheckOptions{ + Location: utils.ImportedLocation, + }, + ) + + require.NoError(t, err) + + _, err = ParseAndCheckWithOptions(t, + ` + import Foo from "imported" + + access(all) fun main() { + var x: &[Int] = Foo.x + Foo.x[0] = 3 + Foo.x.append(4) + + var bar: Foo.Bar = Foo.Bar() + } + `, + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(_ *sema.Checker, _ common.Location, _ ast.Range) (sema.Import, error) { + return sema.ElaborationImport{ + Elaboration: importedChecker.Elaboration, + }, nil + }, + }, + }, + ) + + require.NoError(t, err) +} From 7f390a409a190020c0a5cdfae1a26f8abcd09601 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Tue, 11 Jun 2024 14:14:00 -0700 Subject: [PATCH 2/4] Fix tests --- runtime/coverage_test.go | 59 +++++++++++++++++------ runtime/interpreter/interpreter_import.go | 3 ++ runtime/program_params_validation_test.go | 35 +++++++++++++- runtime/resource_duplicate_test.go | 18 ++++--- runtime/runtime_test.go | 34 +++++++++++-- runtime/tests/checker/import_test.go | 8 ++- runtime/tests/checker/reference_test.go | 2 +- 7 files changed, 130 insertions(+), 29 deletions(-) diff --git a/runtime/coverage_test.go b/runtime/coverage_test.go index 2ec8aa643e..05fed6e3aa 100644 --- a/runtime/coverage_test.go +++ b/runtime/coverage_test.go @@ -32,6 +32,7 @@ import ( "github.com/onflow/cadence/runtime/parser" "github.com/onflow/cadence/runtime/stdlib" . "github.com/onflow/cadence/runtime/tests/runtime_utils" + "github.com/onflow/cadence/runtime/tests/utils" ) func TestRuntimeNewLocationCoverage(t *testing.T) { @@ -1768,7 +1769,7 @@ func TestRuntimeCoverageWithNoStatements(t *testing.T) { t.Parallel() - importedScript := []byte(` + contract := []byte(` access(all) contract FooContract { access(all) resource interface Receiver { } @@ -1776,7 +1777,7 @@ func TestRuntimeCoverageWithNoStatements(t *testing.T) { `) script := []byte(` - import "FooContract" + import FooContract from 0x1 access(all) fun main(): Int { Type<@{FooContract.Receiver}>().identifier return 42 @@ -1784,30 +1785,58 @@ func TestRuntimeCoverageWithNoStatements(t *testing.T) { `) coverageReport := NewCoverageReport() + runtime := NewInterpreterRuntime(Config{ + CoverageReport: coverageReport, + }) - scriptlocation := common.ScriptLocation{0x1b, 0x2c} + scriptLocation := common.ScriptLocation{0x1b, 0x2c} + + transactionLocation := NewTransactionLocationGenerator() + txLocation := transactionLocation() + + authorizers := []Address{{0, 0, 0, 0, 0, 0, 0, 1}} + accountCodes := map[Location][]byte{} runtimeInterface := &TestRuntimeInterface{ - OnGetCode: func(location Location) (bytes []byte, err error) { - switch location { - case common.StringLocation("FooContract"): - return importedScript, nil - default: - return nil, fmt.Errorf("unknown import location: %s", location) - } + Storage: NewTestLedger(nil, nil), + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, + OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { + return accountCodes[location], nil + }, + OnGetSigningAccounts: func() ([]Address, error) { + return authorizers, nil + }, + OnEmitEvent: func(event cadence.Event) error { + return nil }, + OnResolveLocation: NewSingleIdentifierLocationResolver(t), } - runtime := NewInterpreterRuntime(Config{ - CoverageReport: coverageReport, - }) - coverageReport.ExcludeLocation(scriptlocation) + + coverageReport.ExcludeLocation(txLocation) + deploy := utils.DeploymentTransaction("FooContract", contract) + err := runtime.ExecuteTransaction( + Script{ + Source: deploy, + }, + Context{ + Interface: runtimeInterface, + Location: txLocation, + CoverageReport: coverageReport, + }, + ) + require.NoError(t, err) + + coverageReport.ExcludeLocation(scriptLocation) value, err := runtime.ExecuteScript( Script{ Source: script, }, Context{ Interface: runtimeInterface, - Location: scriptlocation, + Location: scriptLocation, CoverageReport: coverageReport, }, ) diff --git a/runtime/interpreter/interpreter_import.go b/runtime/interpreter/interpreter_import.go index 3a6703fe85..d7fb7c236c 100644 --- a/runtime/interpreter/interpreter_import.go +++ b/runtime/interpreter/interpreter_import.go @@ -84,6 +84,9 @@ func (interpreter *Interpreter) importResolvedLocation(resolvedLocation sema.Res for _, name := range names { variable := variables[name] + if variable == nil { + continue + } value := variable.GetValue(interpreter) diff --git a/runtime/program_params_validation_test.go b/runtime/program_params_validation_test.go index 4d3d041672..640bfd3dc6 100644 --- a/runtime/program_params_validation_test.go +++ b/runtime/program_params_validation_test.go @@ -698,18 +698,49 @@ func TestRuntimeTransactionParameterTypeValidation(t *testing.T) { storage := NewTestLedger(nil, nil) + authorizers := []Address{{0, 0, 0, 0, 0, 0, 0, 1}} + accountCodes := map[Location][]byte{} + runtimeInterface := &TestRuntimeInterface{ Storage: storage, OnResolveLocation: NewSingleIdentifierLocationResolver(t), + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { - return contracts[location], nil + return accountCodes[location], nil }, OnDecodeArgument: func(b []byte, t cadence.Type) (value cadence.Value, err error) { return json.Decode(nil, b) }, + OnEmitEvent: func(event cadence.Event) error { + return nil + }, + OnGetSigningAccounts: func() ([]Address, error) { + return authorizers, nil + }, } addPublicKeyValidation(runtimeInterface, nil) + transactionLocation := NewTransactionLocationGenerator() + for location, contract := range contracts { + deploy := DeploymentTransaction(location.Name, contract) + err := rt.ExecuteTransaction( + Script{ + Source: deploy, + }, + Context{ + Interface: runtimeInterface, + Location: transactionLocation(), + }, + ) + + require.NoError(t, err) + } + + authorizers = nil + return rt.ExecuteTransaction( Script{ Source: []byte(script), @@ -717,7 +748,7 @@ func TestRuntimeTransactionParameterTypeValidation(t *testing.T) { }, Context{ Interface: runtimeInterface, - Location: common.TransactionLocation{}, + Location: transactionLocation(), }, ) } diff --git a/runtime/resource_duplicate_test.go b/runtime/resource_duplicate_test.go index 9de1e0fc28..395ecaef49 100644 --- a/runtime/resource_duplicate_test.go +++ b/runtime/resource_duplicate_test.go @@ -29,7 +29,7 @@ import ( "github.com/onflow/cadence/encoding/json" . "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/runtime/common" - "github.com/onflow/cadence/runtime/sema" + "github.com/onflow/cadence/runtime/interpreter" . "github.com/onflow/cadence/runtime/tests/runtime_utils" . "github.com/onflow/cadence/runtime/tests/utils" ) @@ -171,17 +171,21 @@ func TestRuntimeResourceDuplicationWithContractTransfer(t *testing.T) { // Move vault into the contract Holder.setContent(<-vault) - // Save the contract into storage (invalid, even if same account) + // Save the contract reference into storage. + // This won't error, since the validation happens at the end of the transaction. acct.storage.save(Holder as AnyStruct, to: /storage/holder) // Move vault back out of the contract let vault2 <- Holder.swapContent(nil) let unwrappedVault2 <- vault2! - // Load the contract back from storage - let dupeContract = acct.storage.load(from: /storage/holder)! as! Holder + // Load the contract reference back from storage. + // Given the value is a reference, this won't duplicate the contract value. + let dupeContract = acct.storage.load(from: /storage/holder)! as! &Holder - // Move the vault of of the duplicated contract + // Move the vault of of the contract. + // The 'dupeVault' must be nil, since it was moved out of the contract + // in the above step. let dupeVault <- dupeContract.swapContent(nil) let unwrappedDupeVault <- dupeVault! @@ -204,6 +208,6 @@ func TestRuntimeResourceDuplicationWithContractTransfer(t *testing.T) { ) RequireError(t, err) - var invalidMoveError *sema.InvalidMoveError - require.ErrorAs(t, err, &invalidMoveError) + var forceNilError interpreter.ForceNilError + require.ErrorAs(t, err, &forceNilError) } diff --git a/runtime/runtime_test.go b/runtime/runtime_test.go index 8f6eeeacd7..aaba3832c3 100644 --- a/runtime/runtime_test.go +++ b/runtime/runtime_test.go @@ -717,14 +717,21 @@ func TestRuntimeTransactionWithArguments(t *testing.T) { storage := NewTestLedger(nil, nil) + authorizers := []Address{{0, 0, 0, 0, 0, 0, 0, 1}} + accountCodes := map[Location][]byte{} + runtimeInterface := &TestRuntimeInterface{ Storage: storage, OnGetSigningAccounts: func() ([]Address, error) { - return tc.authorizers, nil + return authorizers, nil }, OnResolveLocation: NewSingleIdentifierLocationResolver(t), OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { - return tc.contracts[location], nil + return accountCodes[location], nil + }, + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil }, OnProgramLog: func(message string) { loggedMessages = append(loggedMessages, message) @@ -732,8 +739,29 @@ func TestRuntimeTransactionWithArguments(t *testing.T) { OnDecodeArgument: func(b []byte, t cadence.Type) (cadence.Value, error) { return json.Decode(nil, b) }, + OnEmitEvent: func(event cadence.Event) error { + return nil + }, } + transactionLocation := NewTransactionLocationGenerator() + for location, contract := range tc.contracts { + deploy := DeploymentTransaction(location.Name, contract) + err := rt.ExecuteTransaction( + Script{ + Source: deploy, + }, + Context{ + Interface: runtimeInterface, + Location: transactionLocation(), + }, + ) + + require.NoError(t, err) + } + + authorizers = tc.authorizers + err := rt.ExecuteTransaction( Script{ Source: []byte(tc.script), @@ -741,7 +769,7 @@ func TestRuntimeTransactionWithArguments(t *testing.T) { }, Context{ Interface: runtimeInterface, - Location: common.TransactionLocation{}, + Location: transactionLocation(), }, ) diff --git a/runtime/tests/checker/import_test.go b/runtime/tests/checker/import_test.go index 9d8d87f601..4680f1030e 100644 --- a/runtime/tests/checker/import_test.go +++ b/runtime/tests/checker/import_test.go @@ -769,5 +769,11 @@ func TestCheckImportContract(t *testing.T) { }, ) - require.NoError(t, err) + errs := RequireCheckerErrors(t, err, 2) + + assignmentError := &sema.UnauthorizedReferenceAssignmentError{} + assert.ErrorAs(t, errs[0], &assignmentError) + + accessError := &sema.InvalidAccessError{} + assert.ErrorAs(t, errs[1], &accessError) } diff --git a/runtime/tests/checker/reference_test.go b/runtime/tests/checker/reference_test.go index 446a966403..c9b970f887 100644 --- a/runtime/tests/checker/reference_test.go +++ b/runtime/tests/checker/reference_test.go @@ -1941,7 +1941,7 @@ func TestCheckInvalidatedReferenceUse(t *testing.T) { import Foo from "imported" access(all) fun test() { - let xRef = &Foo.field as &AnyResource + let xRef: &AnyResource = Foo.field xRef } `, From a3820cd5c2ff8e0c458cf0fa13747652ab7018b8 Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Wed, 12 Jun 2024 09:53:43 -0700 Subject: [PATCH 3/4] Add more test --- runtime/import_test.go | 15 +-- runtime/resource_duplicate_test.go | 159 ++++++++++++++++++++++++++- runtime/tests/checker/import_test.go | 130 +++++++++++++++------- runtime/tests/checker/member_test.go | 22 ++++ 4 files changed, 270 insertions(+), 56 deletions(-) diff --git a/runtime/import_test.go b/runtime/import_test.go index 79e01a4a55..9b4de3be58 100644 --- a/runtime/import_test.go +++ b/runtime/import_test.go @@ -425,8 +425,7 @@ func TestRuntimeContractImport(t *testing.T) { return 42 } - access(all) struct Bar { - } + access(all) struct Bar {} init() { self.x = [] @@ -440,8 +439,8 @@ func TestRuntimeContractImport(t *testing.T) { import Foo from 0x01 access(all) fun main() { + var foo: &Foo = Foo var x: &[Int] = Foo.x - var bar: Foo.Bar = Foo.Bar() } `) @@ -499,14 +498,4 @@ func TestRuntimeContractImport(t *testing.T) { }, ) require.NoError(t, err) - - // Script - // - //var checkerErr *sema.CheckerError - //require.ErrorAs(t, err, &checkerErr) - // - //errs := checker.RequireCheckerErrors(t, checkerErr, 1) - // - //var importedProgramErr *sema.ImportedProgramError - //require.ErrorAs(t, errs[0], &importedProgramErr) } diff --git a/runtime/resource_duplicate_test.go b/runtime/resource_duplicate_test.go index 395ecaef49..6e83361c07 100644 --- a/runtime/resource_duplicate_test.go +++ b/runtime/resource_duplicate_test.go @@ -30,11 +30,12 @@ import ( . "github.com/onflow/cadence/runtime" "github.com/onflow/cadence/runtime/common" "github.com/onflow/cadence/runtime/interpreter" + "github.com/onflow/cadence/runtime/sema" . "github.com/onflow/cadence/runtime/tests/runtime_utils" . "github.com/onflow/cadence/runtime/tests/utils" ) -func TestRuntimeResourceDuplicationWithContractTransfer(t *testing.T) { +func TestRuntimeResourceDuplicationWithContractTransferInTransaction(t *testing.T) { t.Parallel() @@ -211,3 +212,159 @@ func TestRuntimeResourceDuplicationWithContractTransfer(t *testing.T) { var forceNilError interpreter.ForceNilError require.ErrorAs(t, err, &forceNilError) } + +func TestRuntimeResourceDuplicationWithContractTransferInSameContract(t *testing.T) { + + t.Parallel() + + runtime := NewTestInterpreterRuntime() + + accountCodes := map[common.Location][]byte{} + + var events []cadence.Event + + signerAccount := common.MustBytesToAddress([]byte{0x1}) + + storage := NewTestLedger(nil, nil) + + runtimeInterface := &TestRuntimeInterface{ + OnGetCode: func(location Location) (bytes []byte, err error) { + return accountCodes[location], nil + }, + Storage: storage, + OnGetSigningAccounts: func() ([]Address, error) { + return []Address{signerAccount}, nil + }, + OnResolveLocation: NewSingleIdentifierLocationResolver(t), + OnGetAccountContractCode: func(location common.AddressLocation) (code []byte, err error) { + return accountCodes[location], nil + }, + OnUpdateAccountContractCode: func(location common.AddressLocation, code []byte) error { + accountCodes[location] = code + return nil + }, + OnEmitEvent: func(event cadence.Event) error { + events = append(events, event) + return nil + }, + OnDecodeArgument: func(b []byte, t cadence.Type) (value cadence.Value, err error) { + return json.Decode(nil, b) + }, + } + + nextTransactionLocation := NewTransactionLocationGenerator() + + // Deploy Fungible Token contract + + err := runtime.ExecuteTransaction( + Script{ + Source: DeploymentTransaction( + "FungibleToken", + []byte(modifiedFungibleTokenContractInterface), + ), + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(t, err) + + // Deploy Flow Token contract + + err = runtime.ExecuteTransaction( + Script{ + Source: []byte(fmt.Sprintf( + ` + transaction { + + prepare(signer: auth(Storage, Contracts, Capabilities) &Account) { + signer.contracts.add(name: "FlowToken", code: "%s".decodeHex(), signer) + } + } + `, + hex.EncodeToString([]byte(modifiedFlowContract)), + )), + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + require.NoError(t, err) + + // Deploy Holder contract + + signerAccount = common.MustBytesToAddress([]byte{0x2}) + + const holderContract = ` + import FlowToken from 0x1 + + access(all) contract Holder { + + access(all) var content: @FlowToken.Vault? + + init() { + self.content <- nil + } + + access(all) fun setContent(_ vault: @FlowToken.Vault?) { + self.content <-! vault + } + + access(all) fun swapContent(_ vault: @FlowToken.Vault?): @FlowToken.Vault? { + let oldVault <- self.content <- vault + return <-oldVault + } + + access(all) fun duplicate(acct: auth(Storage) &Account) { + // Create vault + let vault <- FlowToken.createEmptyVault() as! @FlowToken.Vault? + + // Move vault into the contract + Holder.setContent(<-vault) + + // Save the contract into storage (invalid, even if same account). + // Given here it access the enclosing contract itself (not an imported contract), + // the concrete contract value is available. + acct.storage.save(Holder as AnyStruct, to: /storage/holder) + + // Move vault back out of the contract + let vault2 <- Holder.swapContent(nil) + let unwrappedVault2 <- vault2! + + // Load the contract reference back from storage. + // Given the value is a reference, this won't duplicate the contract value. + let dupeContract = acct.storage.load(from: /storage/holder)! as! &Holder + + // Move the vault of of the contract. + // The 'dupeVault' must be nil, since it was moved out of the contract + // in the above step. + let dupeVault <- dupeContract.swapContent(nil) + let unwrappedDupeVault <- dupeVault! + + // Deposit the duplicated vault into the original vault + unwrappedVault2.deposit(from: <- unwrappedDupeVault) + + destroy unwrappedVault2 + } + + } + ` + err = runtime.ExecuteTransaction( + Script{ + Source: DeploymentTransaction( + "Holder", + []byte(holderContract), + ), + }, + Context{ + Interface: runtimeInterface, + Location: nextTransactionLocation(), + }, + ) + RequireError(t, err) + + var invalidMoveError *sema.InvalidMoveError + require.ErrorAs(t, err, &invalidMoveError) +} diff --git a/runtime/tests/checker/import_test.go b/runtime/tests/checker/import_test.go index 4680f1030e..de971dd7c3 100644 --- a/runtime/tests/checker/import_test.go +++ b/runtime/tests/checker/import_test.go @@ -721,59 +721,105 @@ func TestCheckImportContract(t *testing.T) { t.Parallel() - importedChecker, err := ParseAndCheckWithOptions(t, - ` - access(all) contract Foo { - access(all) let x: [Int] - // access(all) let y: [Int] + t.Run("valid", func(t *testing.T) { - access(all) fun answer(): Int { - return 42 - } + importedChecker, err := ParseAndCheckWithOptions(t, + ` + access(all) contract Foo { + access(all) let x: [Int] - access(all) struct Bar { - } + access(all) fun answer(): Int { + return 42 + } + + access(all) struct Bar {} + + init() { + self.x = [] + } + }`, + ParseAndCheckOptions{ + Location: utils.ImportedLocation, + }, + ) + + require.NoError(t, err) + + _, err = ParseAndCheckWithOptions(t, + ` + import Foo from "imported" - init() { - self.x = [] - // self.y = [] + access(all) fun main() { + var foo: &Foo = Foo + var x: &[Int] = Foo.x + var bar: Foo.Bar = Foo.Bar() } - }`, - ParseAndCheckOptions{ - Location: utils.ImportedLocation, - }, - ) + `, + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(_ *sema.Checker, _ common.Location, _ ast.Range) (sema.Import, error) { + return sema.ElaborationImport{ + Elaboration: importedChecker.Elaboration, + }, nil + }, + }, + }, + ) - require.NoError(t, err) + require.NoError(t, err) + }) - _, err = ParseAndCheckWithOptions(t, - ` - import Foo from "imported" + t.Run("invalid", func(t *testing.T) { - access(all) fun main() { - var x: &[Int] = Foo.x - Foo.x[0] = 3 - Foo.x.append(4) + importedChecker, err := ParseAndCheckWithOptions(t, + ` + access(all) contract Foo { + access(all) let x: [Int] - var bar: Foo.Bar = Foo.Bar() - } - `, - ParseAndCheckOptions{ - Config: &sema.Config{ - ImportHandler: func(_ *sema.Checker, _ common.Location, _ ast.Range) (sema.Import, error) { - return sema.ElaborationImport{ - Elaboration: importedChecker.Elaboration, - }, nil + access(all) fun answer(): Int { + return 42 + } + + access(all) struct Bar {} + + init() { + self.x = [] + } + }`, + ParseAndCheckOptions{ + Location: utils.ImportedLocation, + }, + ) + + require.NoError(t, err) + + _, err = ParseAndCheckWithOptions(t, + ` + import Foo from "imported" + + access(all) fun main() { + Foo.x[0] = 3 + Foo.x.append(4) + } + `, + ParseAndCheckOptions{ + Config: &sema.Config{ + ImportHandler: func(_ *sema.Checker, _ common.Location, _ ast.Range) (sema.Import, error) { + return sema.ElaborationImport{ + Elaboration: importedChecker.Elaboration, + }, nil + }, }, }, - }, - ) + ) - errs := RequireCheckerErrors(t, err, 2) + errs := RequireCheckerErrors(t, err, 2) + + assignmentError := &sema.UnauthorizedReferenceAssignmentError{} + assert.ErrorAs(t, errs[0], &assignmentError) - assignmentError := &sema.UnauthorizedReferenceAssignmentError{} - assert.ErrorAs(t, errs[0], &assignmentError) + accessError := &sema.InvalidAccessError{} + assert.ErrorAs(t, errs[1], &accessError) + }) - accessError := &sema.InvalidAccessError{} - assert.ErrorAs(t, errs[1], &accessError) } diff --git a/runtime/tests/checker/member_test.go b/runtime/tests/checker/member_test.go index f5211a7cc5..b3a0ab4464 100644 --- a/runtime/tests/checker/member_test.go +++ b/runtime/tests/checker/member_test.go @@ -988,3 +988,25 @@ func TestCheckMemberAccess(t *testing.T) { require.NoError(t, err) }) } + +func TestCheckContractFieldAccessInSameContract(t *testing.T) { + t.Parallel() + + _, err := ParseAndCheck(t, ` + contract Foo { + + var array: [Int] + + init() { + self.array = [] + } + + access(all) fun bar() { + // Should return the concrete value, not a reference. + var foo: [Int] = Foo.array + } + }`, + ) + + require.NoError(t, err) +} From f69da859e9d2bdc14bd0090f86fec19765ff0c4d Mon Sep 17 00:00:00 2001 From: Supun Setunga Date: Fri, 14 Jun 2024 10:51:29 -0700 Subject: [PATCH 4/4] Lazily load imported variables --- runtime/interpreter/interpreter_import.go | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/runtime/interpreter/interpreter_import.go b/runtime/interpreter/interpreter_import.go index d7fb7c236c..1318a43466 100644 --- a/runtime/interpreter/interpreter_import.go +++ b/runtime/interpreter/interpreter_import.go @@ -88,19 +88,24 @@ func (interpreter *Interpreter) importResolvedLocation(resolvedLocation sema.Res continue } - value := variable.GetValue(interpreter) + // Lazily load the value + getter := func() Value { + value := variable.GetValue(interpreter) + + // If the variable is a contract value, then import it as a reference. + // This must be done at the type of importing, rather than when declaring the contract value. + compositeValue, ok := value.(*CompositeValue) + if !ok || compositeValue.Kind != common.CompositeKindContract { + return value + } - // If the variable is a contract value, then import it as a reference. - // This must be done at the type of importing, rather than when declaring the contract value. - compositeValue, ok := value.(*CompositeValue) - if ok && compositeValue.Kind == common.CompositeKindContract { staticType := compositeValue.StaticType(interpreter) semaType, err := interpreter.ConvertStaticToSemaType(staticType) if err != nil { panic(err) } - contractReference := NewEphemeralReferenceValue( + return NewEphemeralReferenceValue( interpreter, UnauthorizedAccess, compositeValue, @@ -109,11 +114,10 @@ func (interpreter *Interpreter) importResolvedLocation(resolvedLocation sema.Res Location: interpreter.Location, }, ) - variable = NewVariableWithValue(interpreter, contractReference) } - interpreter.setVariable(name, variable) - interpreter.Globals.Set(name, variable) + importedVariable := NewVariableWithGetter(interpreter, getter) + interpreter.setVariable(name, importedVariable) + interpreter.Globals.Set(name, importedVariable) } - }