diff --git a/cadence/contracts/FlowYieldVaultsClosedBeta.cdc b/cadence/contracts/FlowYieldVaultsClosedBeta.cdc index e544d3f..41f708b 100644 --- a/cadence/contracts/FlowYieldVaultsClosedBeta.cdc +++ b/cadence/contracts/FlowYieldVaultsClosedBeta.cdc @@ -69,6 +69,16 @@ access(all) contract FlowYieldVaultsClosedBeta { return cap } + /// Clean up any previously issued controller before issuing a fresh capability. + access(contract) fun _cleanupExistingGrant(_ addr: Address) { + if let info = self.issuedCapIDs[addr] { + if let ctrl = self.account.capabilities.storage.getController(byCapabilityID: info.capID) { + ctrl.delete() + emit BetaRevoked(addr: addr, capID: info.capID) + } + } + } + /// 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") @@ -83,6 +93,7 @@ access(all) contract FlowYieldVaultsClosedBeta { // 2) A small in-account helper resource that performs privileged ops access(all) resource AdminHandle { access(Admin) fun grantBeta(addr: Address): Capability { + FlowYieldVaultsClosedBeta._cleanupExistingGrant(addr) FlowYieldVaultsClosedBeta._ensureBadge(addr) return FlowYieldVaultsClosedBeta._issueBadgeCap(addr) } diff --git a/cadence/tests/grant_beta_cleanup_test.cdc b/cadence/tests/grant_beta_cleanup_test.cdc new file mode 100644 index 0000000..38dc2f1 --- /dev/null +++ b/cadence/tests/grant_beta_cleanup_test.cdc @@ -0,0 +1,53 @@ +import Test + +import "test_helpers.cdc" + +import "FlowYieldVaultsClosedBeta" + +access(all) let flowYieldVaultsAccount = Test.getAccount(0x0000000000000009) + +access(all) +fun setup() { + deployContracts() +} + +access(all) +fun test_ReGrantBetaRevokesPreviousCapability() { + let user = Test.createAccount() + transferFlow(signer: serviceAccount, recipient: user.address, amount: 1.0) + + grantBeta(flowYieldVaultsAccount, user) + + let backupRes = _executeTransaction("../transactions/test/backup_beta_cap.cdc", [], user) + Test.expect(backupRes, Test.beSucceeded()) + + // Re-granting should revoke the previously issued controller (and thus all old capability copies). + grantBeta(flowYieldVaultsAccount, user) + + // Event assertions: the re-grant should emit BetaRevoked for the *previous* capID, then a fresh BetaGranted. + let grantedAny = Test.eventsOfType(Type()) + var userGrants: [FlowYieldVaultsClosedBeta.BetaGranted] = [] + for evt in grantedAny { + let g = evt as! FlowYieldVaultsClosedBeta.BetaGranted + if g.addr == user.address { + userGrants.append(g) + } + } + Test.assertEqual(2, userGrants.length) + + let revokedAny = Test.eventsOfType(Type()) + var userRevokes: [FlowYieldVaultsClosedBeta.BetaRevoked] = [] + for evt in revokedAny { + let r = evt as! FlowYieldVaultsClosedBeta.BetaRevoked + if r.addr == user.address { + userRevokes.append(r) + } + } + Test.assertEqual(1, userRevokes.length) + Test.assert(userRevokes[0].capID != nil, message: "Expected revoke capID to be non-nil") + Test.assertEqual(userGrants[0].capID, userRevokes[0].capID!) + Test.assert(userGrants[0].capID != userGrants[1].capID, message: "Expected a fresh capID on re-grant") + + let assertRes = _executeTransaction("../transactions/test/assert_backup_beta_cap_revoked.cdc", [], user) + Test.expect(assertRes, Test.beSucceeded()) +} diff --git a/cadence/transactions/test/assert_backup_beta_cap_revoked.cdc b/cadence/transactions/test/assert_backup_beta_cap_revoked.cdc new file mode 100644 index 0000000..f4df0ce --- /dev/null +++ b/cadence/transactions/test/assert_backup_beta_cap_revoked.cdc @@ -0,0 +1,16 @@ +import "FlowYieldVaultsClosedBeta" + +/// Asserts that the beta capability stored at the backup path has been revoked. +transaction { + prepare(signer: auth(Storage, Capabilities) &Account) { + let backupPath = StoragePath(identifier: "FlowYieldVaultsBetaCapBackup")! + + let cap = signer.storage.load< + Capability + >(from: backupPath) + ?? panic("Missing beta capability at backup path \(backupPath)") + + assert(!cap.check(), message: "Expected backup beta capability to be revoked") + } +} + diff --git a/cadence/transactions/test/backup_beta_cap.cdc b/cadence/transactions/test/backup_beta_cap.cdc new file mode 100644 index 0000000..8cdec08 --- /dev/null +++ b/cadence/transactions/test/backup_beta_cap.cdc @@ -0,0 +1,28 @@ +import "FlowYieldVaultsClosedBeta" + +/// Moves the current beta capability from the canonical path to a backup path. +/// Intended for tests that need to retain an "old" beta capability across re-grants. +transaction { + prepare(signer: auth(Storage, Capabilities) &Account) { + let sourcePath = FlowYieldVaultsClosedBeta.UserBetaCapStoragePath + let backupPath = StoragePath(identifier: "FlowYieldVaultsBetaCapBackup")! + + let cap = signer.storage.load< + Capability + >(from: sourcePath) + ?? panic("Missing beta capability at \(sourcePath)") + + if let t = signer.storage.type(at: backupPath) { + if t == Type>() { + let _ = signer.storage.load< + Capability + >(from: backupPath) + } else { + panic("Unexpected type at backup path: ".concat(t.identifier)) + } + } + + signer.storage.save(cap, to: backupPath) + } +} +