Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cadence/contracts/FlowYieldVaults.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,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.assignedTo, betaRef):
"Invalid Beta Ref"
}
return <-create YieldVaultManager()
}
/// Creates a StrategyFactory resource
Expand Down
43 changes: 16 additions & 27 deletions cadence/contracts/FlowYieldVaultsClosedBeta.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@ access(all) contract FlowYieldVaultsClosedBeta {

access(all) resource BetaBadge {
access(all) let assignedTo: Address
init(_ addr: Address) {
self.assignedTo = addr
}
access(all) view fun getOwner(): Address {
return self.assignedTo
init(assignedTo: Address) {
self.assignedTo = assignedTo
}
}

Expand All @@ -22,7 +19,7 @@ access(all) contract FlowYieldVaultsClosedBeta {
access(all) let capID: UInt64
access(all) let isRevoked: Bool

init(_ capID: UInt64, _ isRevoked: Bool) {
init(capID: UInt64, isRevoked: Bool) {
self.capID = capID
self.isRevoked = isRevoked
}
Expand All @@ -35,31 +32,30 @@ access(all) contract FlowYieldVaultsClosedBeta {

/// Per-user badge storage path (under the *contract/deployer* account)
access(contract) fun _badgePath(_ addr: Address): StoragePath {
return StoragePath(identifier: "FlowYieldVaultsBetaBadge_".concat(addr.toString()))!
return StoragePath(identifier: "FlowYieldVaultsBetaBadge_\(addr)")!
}

/// 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)
let path = self._badgePath(addr)
if self.account.storage.type(at: path) == nil {
self.account.storage.save(<-create BetaBadge(assignedTo: addr), to: path)
}
}

access(contract) fun _destroyBadge(_ addr: Address) {
let p = self._badgePath(addr)
if let badge <- self.account.storage.load<@BetaBadge>(from: p) {
let path = self._badgePath(addr)
if let badge <- self.account.storage.load<@BetaBadge>(from: path) {
destroy badge
}
}

/// Issue a capability from the contract/deployer account and record its ID
access(contract) fun _issueBadgeCap(_ addr: Address): Capability<auth(Beta) &BetaBadge> {
let p = self._badgePath(addr)
let cap: Capability<auth(Beta) &BetaBadge> =
self.account.capabilities.storage.issue<auth(Beta) &BetaBadge>(p)
let path = self._badgePath(addr)
let cap = self.account.capabilities.storage.issue<auth(Beta) &BetaBadge>(path)

self.issuedCapIDs[addr] = AccessInfo(cap.id, false)
self.issuedCapIDs[addr] = AccessInfo(capID: cap.id, isRevoked: false)

if let ctrl = self.account.capabilities.storage.getController(byCapabilityID: cap.id) {
ctrl.setTag("flowyieldvaults-beta")
Expand All @@ -85,7 +81,7 @@ access(all) contract FlowYieldVaultsClosedBeta {
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.issuedCapIDs[addr] = AccessInfo(capID: info.capID, isRevoked: true)
self._destroyBadge(addr)
emit BetaRevoked(addr: addr, capID: info.capID)
}
Expand All @@ -107,7 +103,6 @@ access(all) contract FlowYieldVaultsClosedBeta {
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
Expand All @@ -120,16 +115,10 @@ access(all) contract FlowYieldVaultsClosedBeta {
assert(addr == nil, message: "Address is required for Beta verification")
return false
}
let recordedID: UInt64? = self.getBetaCapID(addr!);
if recordedID == nil {
assert(recordedID == nil, message: "No Beta access")
return false
}
let recordedID = self.getBetaCapID(addr!)
assert(recordedID != nil, message: "No Beta access")

if betaRef.getOwner() != addr {
assert(betaRef.getOwner() != addr, message: "BetaBadge may only be used by its assigned owner")
return false
}
assert(betaRef.assignedTo == addr, message: "BetaBadge may only be used by its assigned owner")

return true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// 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(assignedTo: Address) {
self.assignedTo = 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_\(addr)")!
}

/// Ensure the admin-owned badge exists for the user
access(contract) fun _ensureBadge(_ addr: Address) {
let path = self._badgePath(addr)
if self.account.storage.type(at: path) == nil {
self.account.storage.save(<-create BetaBadge(assignedTo: addr), to: path)
}
}

access(contract) fun _destroyBadge(_ addr: Address) {
let path = self._badgePath(addr)
if let badge <- self.account.storage.load<@BetaBadge>(from: path) {
destroy badge
}
}

/// Issue a capability from the contract/deployer account and record its ID
access(contract) fun _issueBadgeCap(_ addr: Address): Capability<auth(Beta) &BetaBadge> {
let path = self._badgePath(addr)
let cap = self.account.capabilities.storage.issue<auth(Beta) &BetaBadge>(path)

self.issuedCapIDs[addr] = AccessInfo(capID: cap.id, isRevoked: 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(capID: info.capID, isRevoked: 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<auth(FlowYieldVaultsClosedBeta.Beta) &FlowYieldVaultsClosedBeta.BetaBadge> {
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 {
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)
}
}
41 changes: 41 additions & 0 deletions cadence/tests/create_yield_vault_manager_validation_test.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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())
Test.assert(res.error != nil, message: "Expected transaction to fail with an error")
let errorMessage = res.error!.message
Test.assert(
errorMessage.contains("Invalid Beta Ref"),
message: "Unexpected error message: ".concat(errorMessage)
)
}
Original file line number Diff line number Diff line change
@@ -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<auth(FlowYieldVaultsClosedBeta.Beta) &FlowYieldVaultsClosedBeta.BetaBadge>
>(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
}
}