From 488fe2c7ad9f2ae946bd3d836cc21cb92d1ffbe4 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Tue, 13 Jan 2026 09:10:14 -0400 Subject: [PATCH 1/2] Fix: Validate betaRef parameter in createYieldVaultManager Add pre-condition to createYieldVaultManager that invokes FlowYieldVaultsClosedBeta.validateBeta to verify the betaRef is valid, making the Beta gating behavior explicit and consistent with other Beta-gated methods. Addresses Quantstamp audit finding FLOW-10. Co-Authored-By: Claude Opus 4.5 --- cadence/contracts/FlowYieldVaults.cdc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cadence/contracts/FlowYieldVaults.cdc b/cadence/contracts/FlowYieldVaults.cdc index cb20b19..3c4a085 100644 --- a/cadence/contracts/FlowYieldVaults.cdc +++ b/cadence/contracts/FlowYieldVaults.cdc @@ -438,6 +438,10 @@ access(all) contract FlowYieldVaults { } /// Creates a YieldVaultManager used to create and manage YieldVaults access(all) fun createYieldVaultManager(betaRef: auth(FlowYieldVaultsClosedBeta.Beta) &FlowYieldVaultsClosedBeta.BetaBadge): @ YieldVaultManager { + pre { + FlowYieldVaultsClosedBeta.validateBeta(betaRef.getOwner(), betaRef): + "Invalid Beta Ref" + } return <-create YieldVaultManager() } /// Creates a StrategyFactory resource From aedf6e9fc1b4adde683958edbd8d8af6831b3357 Mon Sep 17 00:00:00 2001 From: liobrasil Date: Tue, 20 Jan 2026 18:35:09 -0400 Subject: [PATCH 2/2] test: ensure createYieldVaultManager validates betaRef --- ...ldVaultsClosedBeta_validate_beta_false.cdc | 142 ++++++++++++++++++ ...te_yield_vault_manager_validation_test.cdc | 36 +++++ ...eate_yield_vault_manager_with_beta_cap.cdc | 23 +++ 3 files changed, 201 insertions(+) create mode 100644 cadence/contracts/mocks/FlowYieldVaultsClosedBeta_validate_beta_false.cdc create mode 100644 cadence/tests/create_yield_vault_manager_validation_test.cdc create mode 100644 cadence/transactions/test/create_yield_vault_manager_with_beta_cap.cdc diff --git a/cadence/contracts/mocks/FlowYieldVaultsClosedBeta_validate_beta_false.cdc b/cadence/contracts/mocks/FlowYieldVaultsClosedBeta_validate_beta_false.cdc new file mode 100644 index 0000000..91e5faf --- /dev/null +++ b/cadence/contracts/mocks/FlowYieldVaultsClosedBeta_validate_beta_false.cdc @@ -0,0 +1,142 @@ +// TEST-ONLY MOCK CONTRACT. +// +// Some unit tests need a *well-typed* beta reference +// (`auth(FlowYieldVaultsClosedBeta.Beta) & FlowYieldVaultsClosedBeta.BetaBadge`) +// that fails validation, to prove that a call-site actually invokes +// `FlowYieldVaultsClosedBeta.validateBeta(...)`. +// +// In Cadence, resources like `BetaBadge` can only be created by the contract that +// declares them, which makes it difficult to "forge" an invalid badge/reference +// in a transaction. +// +// To keep tests deterministic, this file redeploys the `FlowYieldVaultsClosedBeta` +// contract with `validateBeta` hardcoded to return `false` for all inputs. The +// rest of the contract is kept aligned with `cadence/contracts/FlowYieldVaultsClosedBeta.cdc`. +// +// DO NOT deploy this mock to any network. +access(all) contract FlowYieldVaultsClosedBeta { + + access(all) entitlement Admin + access(all) entitlement Beta + + access(all) resource BetaBadge { + access(all) let assignedTo: Address + init(_ addr: Address) { + self.assignedTo = addr + } + access(all) view fun getOwner(): Address { + return self.assignedTo + } + } + + // --- Paths --- + access(all) let UserBetaCapStoragePath: StoragePath + access(all) let AdminHandleStoragePath: StoragePath + + // --- Registry: which capability was issued to which address, and revocation flags --- + access(all) struct AccessInfo { + access(all) let capID: UInt64 + access(all) let isRevoked: Bool + + init(_ capID: UInt64, _ isRevoked: Bool) { + self.capID = capID + self.isRevoked = isRevoked + } + } + access(all) var issuedCapIDs: {Address: AccessInfo} + + // --- Events --- + access(all) event BetaGranted(addr: Address, capID: UInt64) + access(all) event BetaRevoked(addr: Address, capID: UInt64?) + + /// Per-user badge storage path (under the *contract/deployer* account) + access(contract) fun _badgePath(_ addr: Address): StoragePath { + return StoragePath(identifier: "FlowYieldVaultsBetaBadge_".concat(addr.toString()))! + } + + /// Ensure the admin-owned badge exists for the user + access(contract) fun _ensureBadge(_ addr: Address) { + let p = self._badgePath(addr) + if self.account.storage.type(at: p) == nil { + self.account.storage.save(<-create BetaBadge(addr), to: p) + } + } + + access(contract) fun _destroyBadge(_ addr: Address) { + let p = self._badgePath(addr) + if let badge <- self.account.storage.load<@BetaBadge>(from: p) { + destroy badge + } + } + + /// Issue a capability from the contract/deployer account and record its ID + access(contract) fun _issueBadgeCap(_ addr: Address): Capability { + let p = self._badgePath(addr) + let cap: Capability = + self.account.capabilities.storage.issue(p) + + self.issuedCapIDs[addr] = AccessInfo(cap.id, false) + + if let ctrl = self.account.capabilities.storage.getController(byCapabilityID: cap.id) { + ctrl.setTag("flowyieldvaults-beta") + } + + emit BetaGranted(addr: addr, capID: cap.id) + return cap + } + + /// Delete the recorded controller, revoking *all copies* of the capability + access(contract) fun _revokeByAddress(_ addr: Address) { + let info = self.issuedCapIDs[addr] ?? panic("No cap recorded for address") + let ctrl = self.account.capabilities.storage.getController(byCapabilityID: info.capID) + ?? panic("Missing controller for recorded cap ID") + ctrl.delete() + self.issuedCapIDs[addr] = AccessInfo(info.capID, true) + self._destroyBadge(addr) + emit BetaRevoked(addr: addr, capID: info.capID) + } + + // 2) A small in-account helper resource that performs privileged ops + access(all) resource AdminHandle { + access(Admin) fun grantBeta(addr: Address): Capability { + FlowYieldVaultsClosedBeta._ensureBadge(addr) + return FlowYieldVaultsClosedBeta._issueBadgeCap(addr) + } + + access(Admin) fun revokeByAddress(addr: Address) { + FlowYieldVaultsClosedBeta._revokeByAddress(addr) + } + } + + /// Read-only check used by any gated entrypoint + access(all) view fun getBetaCapID(_ addr: Address): UInt64? { + if let info = self.issuedCapIDs[addr] { + if info.isRevoked { + assert(info.isRevoked, message: "Beta access revoked") + return nil + } + return info.capID + } + return nil + } + + // TEST-ONLY: Always invalid, regardless of address or reference. + // Used to ensure beta-gated entrypoints actually call `validateBeta`. + access(all) view fun validateBeta(_ addr: Address?, _ betaRef: auth(Beta) &BetaBadge): Bool { + return false + } + + init() { + self.AdminHandleStoragePath = StoragePath( + identifier: "FlowYieldVaultsClosedBetaAdmin_\(self.account.address)" + )! + self.UserBetaCapStoragePath = StoragePath( + identifier: "FlowYieldVaultsUserBetaCap_\(self.account.address)" + )! + + self.issuedCapIDs = {} + + // Create and store the admin handle in *this* (deployer) account + self.account.storage.save(<-create AdminHandle(), to: self.AdminHandleStoragePath) + } +} diff --git a/cadence/tests/create_yield_vault_manager_validation_test.cdc b/cadence/tests/create_yield_vault_manager_validation_test.cdc new file mode 100644 index 0000000..d5e58c8 --- /dev/null +++ b/cadence/tests/create_yield_vault_manager_validation_test.cdc @@ -0,0 +1,36 @@ +import Test + +import "test_helpers.cdc" + +access(all) let flowYieldVaultsAccount = Test.getAccount(0x0000000000000009) + +access(all) +fun setup() { + deployContracts() +} + +access(all) +fun test_CreateYieldVaultManagerValidatesBetaRef() { + // Swap in a test-only FlowYieldVaultsClosedBeta implementation where `validateBeta` always returns false. + // This lets us assert that `FlowYieldVaults.createYieldVaultManager` actually calls `validateBeta`. + let err = Test.deployContract( + name: "FlowYieldVaultsClosedBeta", + path: "../contracts/mocks/FlowYieldVaultsClosedBeta_validate_beta_false.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + let user = Test.createAccount() + transferFlow(signer: serviceAccount, recipient: user.address, amount: 1.0) + grantBeta(flowYieldVaultsAccount, user) + + let txn = Test.Transaction( + code: Test.readFile("../transactions/test/create_yield_vault_manager_with_beta_cap.cdc"), + authorizers: [user.address], + signers: [user], + arguments: [] + ) + let res = Test.executeTransaction(txn) + Test.expect(res, Test.beFailed()) +} + diff --git a/cadence/transactions/test/create_yield_vault_manager_with_beta_cap.cdc b/cadence/transactions/test/create_yield_vault_manager_with_beta_cap.cdc new file mode 100644 index 0000000..6f98c53 --- /dev/null +++ b/cadence/transactions/test/create_yield_vault_manager_with_beta_cap.cdc @@ -0,0 +1,23 @@ +import "FlowYieldVaults" +import "FlowYieldVaultsClosedBeta" + +/// Creates (and destroys) a YieldVaultManager using the caller's stored beta capability. +transaction { + let betaRef: auth(FlowYieldVaultsClosedBeta.Beta) &FlowYieldVaultsClosedBeta.BetaBadge + + prepare(signer: auth(BorrowValue, CopyValue) &Account) { + let betaCap = signer.storage.copy< + Capability + >(from: FlowYieldVaultsClosedBeta.UserBetaCapStoragePath) + ?? panic("Missing Beta capability at \(FlowYieldVaultsClosedBeta.UserBetaCapStoragePath)") + + self.betaRef = betaCap.borrow() + ?? panic("Beta capability does not contain correct reference") + } + + execute { + let manager <- FlowYieldVaults.createYieldVaultManager(betaRef: self.betaRef) + destroy manager + } +} +