From 64a60b51f7e958a7c273f553c868b66c68390c2a Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 23 Dec 2025 14:54:54 -0800 Subject: [PATCH 1/4] Add legacy `Crypto` identifier to linter --- internal/cadence/linter.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/internal/cadence/linter.go b/internal/cadence/linter.go index a1603e94b..fa2fb2677 100644 --- a/internal/cadence/linter.go +++ b/internal/cadence/linter.go @@ -35,6 +35,7 @@ import ( "github.com/onflow/cadence/sema" "github.com/onflow/cadence/stdlib" "github.com/onflow/cadence/tools/analysis" + "github.com/onflow/flow-core-contracts/lib/go/contracts" "github.com/onflow/flowkit/v2" "golang.org/x/exp/maps" ) @@ -319,6 +320,41 @@ func (l *linter) handleImport( return sema.ElaborationImport{ Elaboration: helpersChecker.Elaboration, }, nil + case stdlib.CryptoContractLocation: + cryptoChecker, ok := l.checkers[stdlib.CryptoContractLocation.String()] + if !ok { + cryptoCode := contracts.Crypto() + cryptoProgram, err := parser.ParseProgram(nil, cryptoCode, parser.Config{}) + if err != nil { + return nil, err + } + if cryptoProgram == nil { + return nil, &sema.CheckerError{ + Errors: []error{fmt.Errorf("cannot parse Crypto contract")}, + } + } + + cryptoChecker, err = sema.NewChecker( + cryptoProgram, + stdlib.CryptoContractLocation, + nil, + l.checkerStandardConfig, + ) + if err != nil { + return nil, err + } + + err = cryptoChecker.Check() + if err != nil { + return nil, err + } + + l.checkers[stdlib.CryptoContractLocation.String()] = cryptoChecker + } + + return sema.ElaborationImport{ + Elaboration: cryptoChecker.Elaboration, + }, nil default: // Normalize relative path imports to absolute paths if util.IsPathLocation(importedLocation) { From 3b8aa3cb1dbd209a7b3cd81f4409a682a7866ee4 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Tue, 23 Dec 2025 15:34:43 -0800 Subject: [PATCH 2/4] add test --- internal/cadence/lint_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/internal/cadence/lint_test.go b/internal/cadence/lint_test.go index 44729d243..b39c932fa 100644 --- a/internal/cadence/lint_test.go +++ b/internal/cadence/lint_test.go @@ -300,6 +300,28 @@ func Test_Lint(t *testing.T) { ) }) + t.Run("resolves stdlib imports Crypto", func(t *testing.T) { + t.Parallel() + + state := setupMockState(t) + + results, err := lintFiles(state, "StdlibImportsCrypto.cdc") + require.NoError(t, err) + + require.Equal(t, + &lintResult{ + Results: []fileResult{ + { + FilePath: "StdlibImportsCrypto.cdc", + Diagnostics: []analysis.Diagnostic{}, + }, + }, + exitCode: 0, + }, + results, + ) + }) + t.Run("resolves nested imports when contract imported by name", func(t *testing.T) { t.Parallel() @@ -392,6 +414,15 @@ func setupMockState(t *testing.T) *flowkit.State { let foo = getAuthAccount<&Account>(0x01) log(RLP.getType()) }`), 0644) + _ = afero.WriteFile(mockFs, "StdlibImportsCrypto.cdc", []byte(` + import Crypto + + access(all) contract CryptoImportTest { + access(all) fun test(): Void { + let _ = Crypto.hash([1, 2, 3], algorithm: HashAlgorithm.SHA3_256) + } + } + `), 0644) // Regression test files for nested import bug _ = afero.WriteFile(mockFs, "Helper.cdc", []byte(` From 3b68dd8a8997d4cbe5404316c9857c7a7bf2539f Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Mon, 29 Dec 2025 14:29:08 -0800 Subject: [PATCH 3/4] Fix account access resolution for project dependency contracts --- internal/cadence/lint_test.go | 249 ++++++++++++++++++++++++++++++++++ internal/cadence/linter.go | 46 ++++--- 2 files changed, 277 insertions(+), 18 deletions(-) diff --git a/internal/cadence/lint_test.go b/internal/cadence/lint_test.go index b39c932fa..0d73471ba 100644 --- a/internal/cadence/lint_test.go +++ b/internal/cadence/lint_test.go @@ -24,6 +24,7 @@ import ( "github.com/onflow/cadence/ast" "github.com/onflow/cadence/common" "github.com/onflow/cadence/tools/analysis" + flowsdk "github.com/onflow/flow-go-sdk" "github.com/onflow/flowkit/v2" "github.com/onflow/flowkit/v2/config" "github.com/spf13/afero" @@ -343,6 +344,68 @@ func Test_Lint(t *testing.T) { results, ) }) + + t.Run("allows access(account) when contracts on same account", func(t *testing.T) { + t.Parallel() + + state := setupMockStateWithAccountAccess(t) + + results, err := lintFiles(state, "ContractA.cdc") + require.NoError(t, err) + + // Should have no errors since ContractA and ContractB are on same account + require.Equal(t, + &lintResult{ + Results: []fileResult{ + { + FilePath: "ContractA.cdc", + Diagnostics: []analysis.Diagnostic{}, + }, + }, + exitCode: 0, + }, + results, + ) + }) + + t.Run("denies access(account) when contracts on different accounts", func(t *testing.T) { + t.Parallel() + + state := setupMockStateWithAccountAccess(t) + + results, err := lintFiles(state, "ContractC.cdc") + require.NoError(t, err) + + // Should have error since ContractC and ContractB are on different accounts + require.Len(t, results.Results, 1) + require.Len(t, results.Results[0].Diagnostics, 1) + require.Equal(t, "semantic-error", results.Results[0].Diagnostics[0].Category) + require.Contains(t, results.Results[0].Diagnostics[0].Message, "access denied") + require.Equal(t, 1, results.exitCode) + }) + + t.Run("allows access(account) when dependencies on same account (peak-money repro)", func(t *testing.T) { + t.Parallel() + + state := setupMockStateWithDependencies(t) + + results, err := lintFiles(state, "imports/testaddr/DepA.cdc") + require.NoError(t, err) + + // Should have no errors since DepA and DepB are dependencies on same address + require.Equal(t, + &lintResult{ + Results: []fileResult{ + { + FilePath: "imports/testaddr/DepA.cdc", + Diagnostics: []analysis.Diagnostic{}, + }, + }, + exitCode: 0, + }, + results, + ) + }) } func setupMockState(t *testing.T) *flowkit.State { @@ -480,3 +543,189 @@ func setupMockState(t *testing.T) *flowkit.State { return state } + +func setupMockStateWithAccountAccess(t *testing.T) *flowkit.State { + // Mock file system + mockFs := afero.NewMemMapFs() + + // ContractB has an access(account) function + _ = afero.WriteFile(mockFs, "ContractB.cdc", []byte(` + access(all) contract ContractB { + access(account) fun accountOnlyFunction() { + log("This requires account access") + } + init() {} + } + `), 0644) + + // ContractA imports and calls ContractB's account function - should work (same account) + _ = afero.WriteFile(mockFs, "ContractA.cdc", []byte(` + import ContractB from "ContractB" + + access(all) contract ContractA { + access(all) fun callB() { + ContractB.accountOnlyFunction() + } + init() {} + } + `), 0644) + + // ContractC imports and calls ContractB's account function - should fail (different account) + _ = afero.WriteFile(mockFs, "ContractC.cdc", []byte(` + import ContractB from "ContractB" + + access(all) contract ContractC { + access(all) fun callB() { + ContractB.accountOnlyFunction() + } + init() {} + } + `), 0644) + + rw := afero.Afero{Fs: mockFs} + state, err := flowkit.Init(rw) + require.NoError(t, err) + + // Configure contracts with deployments + // ContractA and ContractB are on the same account (0x01) + state.Contracts().AddOrUpdate(config.Contract{ + Name: "ContractA", + Location: "ContractA.cdc", + Aliases: config.Aliases{ + { + Network: "testnet", + Address: flowsdk.HexToAddress("0000000000000001"), + }, + }, + }) + + state.Contracts().AddOrUpdate(config.Contract{ + Name: "ContractB", + Location: "ContractB.cdc", + Aliases: config.Aliases{ + { + Network: "testnet", + Address: flowsdk.HexToAddress("0000000000000001"), + }, + }, + }) + + // ContractC is on a different account (0x02) + state.Contracts().AddOrUpdate(config.Contract{ + Name: "ContractC", + Location: "ContractC.cdc", + Aliases: config.Aliases{ + { + Network: "testnet", + Address: flowsdk.HexToAddress("0000000000000002"), + }, + }, + }) + + // Add network + state.Networks().AddOrUpdate(config.Network{ + Name: "testnet", + Host: "access.testnet.nodes.onflow.org:9000", + }) + + return state +} + +func setupMockStateWithDependencies(t *testing.T) *flowkit.State { + // Reproduce peak-money structure: dependencies with aliases, not contracts + mockFs := afero.NewMemMapFs() + + // DepB has an access(account) function (like FlowEVMBridgeCustomAssociations) + _ = afero.WriteFile(mockFs, "imports/testaddr/DepB.cdc", []byte(` + access(all) contract DepB { + access(account) fun pauseConfig(forType: Type) { + log("This requires account access") + } + init() {} + } + `), 0644) + + // DepA imports and calls DepB's account function (like FlowEVMBridgeConfig) + _ = afero.WriteFile(mockFs, "imports/testaddr/DepA.cdc", []byte(` + import DepB from "DepB" + + access(all) contract DepA { + access(all) fun callDepB(forType: Type) { + DepB.pauseConfig(forType: forType) + } + init() {} + } + `), 0644) + + rw := afero.Afero{Fs: mockFs} + state, err := flowkit.Init(rw) + require.NoError(t, err) + + // Add network first + state.Networks().AddOrUpdate(config.Network{ + Name: "testnet", + Host: "access.testnet.nodes.onflow.org:9000", + }) + + // Add as DEPENDENCIES (not contracts) with same aliases - this is the key difference from peak-money + state.Dependencies().AddOrUpdate(config.Dependency{ + Name: "DepA", + Source: config.Source{ + NetworkName: "testnet", + Address: flowsdk.HexToAddress("dfc20aee650fcbdf"), + ContractName: "DepA", + }, + Aliases: config.Aliases{ + { + Network: "testnet", + Address: flowsdk.HexToAddress("dfc20aee650fcbdf"), + }, + { + Network: "emulator", + Address: flowsdk.HexToAddress("f8d6e0586b0a20c7"), + }, + }, + }) + + state.Dependencies().AddOrUpdate(config.Dependency{ + Name: "DepB", + Source: config.Source{ + NetworkName: "testnet", + Address: flowsdk.HexToAddress("dfc20aee650fcbdf"), + ContractName: "DepB", + }, + Aliases: config.Aliases{ + { + Network: "testnet", + Address: flowsdk.HexToAddress("dfc20aee650fcbdf"), + }, + { + Network: "emulator", + Address: flowsdk.HexToAddress("f8d6e0586b0a20c7"), + }, + }, + }) + + // Dependencies should also be added as contracts for import resolution + // This is what happens when you run `flow dependencies install` + state.Contracts().AddDependencyAsContract( + *state.Dependencies().ByName("DepA"), + "testnet", + ) + state.Contracts().AddDependencyAsContract( + *state.Dependencies().ByName("DepB"), + "testnet", + ) + + // Set the Location field so imports can resolve the files + depAContract, _ := state.Contracts().ByName("DepA") + if depAContract != nil { + depAContract.Location = "imports/testaddr/DepA.cdc" + } + depBContract, _ := state.Contracts().ByName("DepB") + if depBContract != nil { + depBContract.Location = "imports/testaddr/DepB.cdc" + } + + return state +} diff --git a/internal/cadence/linter.go b/internal/cadence/linter.go index fa2fb2677..e186f0567 100644 --- a/internal/cadence/linter.go +++ b/internal/cadence/linter.go @@ -36,6 +36,7 @@ import ( "github.com/onflow/cadence/stdlib" "github.com/onflow/cadence/tools/analysis" "github.com/onflow/flow-core-contracts/lib/go/contracts" + flowGo "github.com/onflow/flow-go-sdk" "github.com/onflow/flowkit/v2" "golang.org/x/exp/maps" ) @@ -244,29 +245,38 @@ func (l *linter) checkAccountAccess(checker *sema.Checker, memberLocation common return false } - checkerContract, err := l.state.Contracts().ByName(checkerContractName) - if err != nil { - return false - } - - memberContract, err := l.state.Contracts().ByName(memberContractName) - if err != nil { - return false - } - + // Build contract name -> address mapping per network for _, network := range *networks { - checkerAddr, err := l.state.ContractAddress(checkerContract, network) - if err != nil || checkerAddr == nil { - continue + contractNameToAddress := make(map[string]flowGo.Address) + + // Add aliases first + contracts := l.state.Contracts() + if contracts != nil { + for _, contract := range *contracts { + if alias := contract.Aliases.ByNetwork(network.Name); alias != nil { + contractNameToAddress[contract.Name] = alias.Address + } + } } - memberAddr, err := l.state.ContractAddress(memberContract, network) - if err != nil || memberAddr == nil { - continue + // Add deployments (overwrites aliases, giving deployments priority) + deployedContracts, err := l.state.DeploymentContractsByNetwork(network) + if err == nil { + for _, deployedContract := range deployedContracts { + contract, err := l.state.Contracts().ByName(deployedContract.Name) + if err == nil { + address, err := l.state.ContractAddress(contract, network) + if err == nil && address != nil { + contractNameToAddress[deployedContract.Name] = *address + } + } + } } - // If they're on the same account for this network, allow access - if *checkerAddr == *memberAddr { + // Check if both contracts exist at the same address on this network + checkerAddress, checkerExists := contractNameToAddress[checkerContractName] + memberAddress, memberExists := contractNameToAddress[memberContractName] + if checkerExists && memberExists && checkerAddress == memberAddress { return true } } From 95b1e1e34c3a29f03f32dbe1c8dc88d31eed2990 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Mon, 29 Dec 2025 14:49:15 -0800 Subject: [PATCH 4/4] add extra edge case test --- internal/cadence/lint_test.go | 112 ++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/internal/cadence/lint_test.go b/internal/cadence/lint_test.go index 0d73471ba..7628fc18d 100644 --- a/internal/cadence/lint_test.go +++ b/internal/cadence/lint_test.go @@ -406,6 +406,38 @@ func Test_Lint(t *testing.T) { results, ) }) + + t.Run("allows access(account) when dependencies have Source but no Aliases", func(t *testing.T) { + t.Parallel() + + state := setupMockStateWithSourceOnly(t) + + // Verify that AddDependencyAsContract automatically adds Source to Aliases + sourceAContract, _ := state.Contracts().ByName("SourceA") + require.NotNil(t, sourceAContract, "SourceA contract should exist") + + // Check if the alias was automatically added from Source + alias := sourceAContract.Aliases.ByNetwork("testnet") + require.NotNil(t, alias, "Alias should be automatically created from Source") + require.Equal(t, "dfc20aee650fcbdf", alias.Address.String(), "Alias address should match Source address") + + results, err := lintFiles(state, "imports/testaddr/SourceA.cdc") + require.NoError(t, err) + + // Should have no errors since SourceA and SourceB have same Source.Address (converted to Aliases) + require.Equal(t, + &lintResult{ + Results: []fileResult{ + { + FilePath: "imports/testaddr/SourceA.cdc", + Diagnostics: []analysis.Diagnostic{}, + }, + }, + exitCode: 0, + }, + results, + ) + }) } func setupMockState(t *testing.T) *flowkit.State { @@ -729,3 +761,83 @@ func setupMockStateWithDependencies(t *testing.T) *flowkit.State { return state } + +func setupMockStateWithSourceOnly(t *testing.T) *flowkit.State { + // Test dependencies with ONLY Source (no Aliases) to see if we need to check Source + mockFs := afero.NewMemMapFs() + + // SourceB has an access(account) function + _ = afero.WriteFile(mockFs, "imports/testaddr/SourceB.cdc", []byte(` + access(all) contract SourceB { + access(account) fun sourceOnlyFunction() { + log("This requires account access") + } + init() {} + } + `), 0644) + + // SourceA imports and calls SourceB's account function + _ = afero.WriteFile(mockFs, "imports/testaddr/SourceA.cdc", []byte(` + import SourceB from "SourceB" + + access(all) contract SourceA { + access(all) fun callSourceB() { + SourceB.sourceOnlyFunction() + } + init() {} + } + `), 0644) + + rw := afero.Afero{Fs: mockFs} + state, err := flowkit.Init(rw) + require.NoError(t, err) + + // Add network + state.Networks().AddOrUpdate(config.Network{ + Name: "testnet", + Host: "access.testnet.nodes.onflow.org:9000", + }) + + // Add dependencies with ONLY Source, NO Aliases + state.Dependencies().AddOrUpdate(config.Dependency{ + Name: "SourceA", + Source: config.Source{ + NetworkName: "testnet", + Address: flowsdk.HexToAddress("dfc20aee650fcbdf"), + ContractName: "SourceA", + }, + // NO Aliases! + }) + + state.Dependencies().AddOrUpdate(config.Dependency{ + Name: "SourceB", + Source: config.Source{ + NetworkName: "testnet", + Address: flowsdk.HexToAddress("dfc20aee650fcbdf"), + ContractName: "SourceB", + }, + // NO Aliases! + }) + + // Add as contracts for import resolution + state.Contracts().AddDependencyAsContract( + *state.Dependencies().ByName("SourceA"), + "testnet", + ) + state.Contracts().AddDependencyAsContract( + *state.Dependencies().ByName("SourceB"), + "testnet", + ) + + // Set the Location field so imports can resolve + sourceAContract, _ := state.Contracts().ByName("SourceA") + if sourceAContract != nil { + sourceAContract.Location = "imports/testaddr/SourceA.cdc" + } + sourceBContract, _ := state.Contracts().ByName("SourceB") + if sourceBContract != nil { + sourceBContract.Location = "imports/testaddr/SourceB.cdc" + } + + return state +}