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
7 changes: 1 addition & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,7 @@ import Test
import BlockchainHelpers

access(all) var snapshot: UInt64 = 0

access(all) fun beforeEach() {
if snapshot != getCurrentBlockHeight() {
Test.reset(to: snapshot)
}
}
access(all) fun beforeEach() { Test.reset(to: snapshot) }

access(all) fun setup() {
// deploy contracts
Expand Down
32 changes: 32 additions & 0 deletions cadence/contracts/yield_vaults/FlowYieldVaults.cdc
Original file line number Diff line number Diff line change
@@ -1,4 +1,36 @@
import "FungibleToken"

access(all) contract FlowYieldVaults {

access(all) struct interface Strategy {
access(all) fun createYieldVault(strategyID: UInt64): @YieldVault
}

access(all) resource YieldVault: FungibleToken.Provider, FungibleToken.Receiver {


access(all) view fun isAvailableToWithdraw(amount _: UFix64): Bool {
panic("TODO")
}

access(FungibleToken.Withdraw) fun withdraw(amount _: UFix64): @{FungibleToken.Vault} {
panic("TODO")
}

access(all) fun deposit(from _: @{FungibleToken.Vault}) {
panic("TODO")
}

access(all) view fun getSupportedVaultTypes(): {Type: Bool} {
panic("TODO")
}

access(all) view fun isSupportedVaultType(type _: Type): Bool {
panic("TODO")
}
}

access(account) fun createYieldVault(strategyID _: UInt64): @YieldVault {
panic("not implemented")
}
}
174 changes: 174 additions & 0 deletions cadence/contracts/yield_vaults/FlowYieldVaultsEarlyAccess.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import "FlowYieldVaults"

/// Gates yield vault creation during the early access period.
/// An `Admin` resource issues and manages `EarlyAccessPass` resources,
/// keyed by recipient address: each address has at most one pass.
/// Pass holders call `createYieldVault` to create yield vaults
/// until their allowance is exhausted.
access(all) contract FlowYieldVaultsEarlyAccess {
Comment thread
holyfuchs marked this conversation as resolved.

/// Emitted when a pass is issued (or re-issued) to an address.
access(all) event PassIssued(addr: Address, allowance: UInt64)
/// Emitted when a pass is revoked and destroyed by the admin.
access(all) event PassRevoked(addr: Address)
/// Emitted when a pass is used to create a yield vault.
access(all) event PassUsed(addr: Address, remainingAllowance: UInt64)

/// Storage path where the `Admin` resource is saved.
access(all) let adminStoragePath: StoragePath
/// Storage path where pass capabilities are stored for claiming.
access(all) let passCapabilityStoragePath: StoragePath

/// Held in the holder's storage; gates yield vault creation during early access.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Held in the holder's storage; gates yield vault creation during early access.
/// Held in the admin's storage; capability granted to user; gates yield vault creation during early access.

access(all) resource EarlyAccessPass {
/// Address this pass was issued to; stamped at creation and never changes.
access(all) let addr: Address
Comment on lines +24 to +25
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this address is purely informational for off-chain purposes? Is it actually needed off-chain?

/// Number of yield vaults the holder may still create.
access(all) var remainingAllowance: UInt64

/// Consumes one unit of allowance and creates a new yield vault.
/// Panics if allowance is exhausted.
///
/// **Parameters**
/// - `strategyID`: Identifies the vault strategy to create.
///
/// **Returns** A new `YieldVault` to be saved in the caller's storage.
access(all) fun createYieldVault(strategyID: UInt64): @FlowYieldVaults.YieldVault {
pre { self.remainingAllowance > 0: "No remaining allowance" }
self.remainingAllowance = self.remainingAllowance - 1
let vault <- FlowYieldVaults.createYieldVault(strategyID: strategyID)
emit PassUsed(addr: self.addr, remainingAllowance: self.remainingAllowance)
return <- vault
}

access(contract) fun setAllowance(_ newAllowance: UInt64) {
self.remainingAllowance = newAllowance
}

init(addr: Address, allowance: UInt64) {
self.addr = addr
self.remainingAllowance = allowance
}
}

access(all) resource Admin {
/// Issues a pass to `addr` and publishes the capability to their inbox.
/// If a pass already exists for `addr`, its `remainingAllowance` is
/// replaced with the new `allowance`, and any previously issued
/// capabilities for that pass are invalidated.
///
/// **Parameters**
/// - `addr`: Recipient who will claim the pass from their inbox.
/// - `allowance`: Number of yield vaults the pass holder may create.
access(all) fun issuePass(to addr: Address, allowance: UInt64) {
let path = FlowYieldVaultsEarlyAccess.passStoragePath(addr: addr)
if FlowYieldVaultsEarlyAccess.checkPass(addr: addr) {
let pass = FlowYieldVaultsEarlyAccess.borrowPass(addr: addr)
Comment on lines +65 to +66
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just have borrowPass return an optional instead of panicing, and then checking is not needed here

pass.setAllowance(allowance)
FlowYieldVaultsEarlyAccess.deletePassCapabilities(addr: addr)
} else {
let pass <- create EarlyAccessPass(addr: addr, allowance: allowance)
FlowYieldVaultsEarlyAccess.account.storage.save(<- pass, to: path)
}
FlowYieldVaultsEarlyAccess.publishPassCapability(addr: addr)
emit PassIssued(addr: addr, allowance: allowance)
}

/// Destroys the pass, deletes its capability controllers, and retracts
/// the inbox entry if still unclaimed. Panics if no pass exists for `addr`.
/// Any previously claimed capability becomes dead (`borrow()` returns `nil`).
///
/// **Parameters**
/// - `addr`: Recipient whose pass should be revoked.
access(all) fun revokePass(addr: Address) {
let pass <- FlowYieldVaultsEarlyAccess.loadPass(addr: addr)
destroy pass
FlowYieldVaultsEarlyAccess.deletePassCapabilities(addr: addr)
FlowYieldVaultsEarlyAccess.unpublishPassCapability(addr: addr)
emit PassRevoked(addr: addr)
}

/// Replaces the remaining allowance on an existing pass.
/// Panics if no pass exists for `addr`.
///
/// **Parameters**
/// - `addr`: Recipient whose pass allowance should be updated.
/// - `newAllowance`: New vault budget; `0` immediately blocks creation.
access(all) fun setAllowance(addr: Address, newAllowance: UInt64) {
let pass = FlowYieldVaultsEarlyAccess.borrowPass(addr: addr)
pass.setAllowance(newAllowance)
}

}

/// Returns whether a pass is currently held for the given address.
///
/// **Parameters**
/// - `addr`: Recipient to check.
///
/// **Returns** `true` if a pass exists for `addr`, `false` otherwise.
view access(all) fun passExists(addr: Address): Bool {
return self.checkPass(addr: addr)
}
Comment on lines +104 to +112
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function seems redundant. However, passExists is IMHO better named than checkPass (makes me wonder: what does it "check"?). So maybe just inline checkPass here and switch all existing uses of checkPass to passExists


/// Returns the remaining allowance of the pass issued to `addr`.
/// Panics if no pass exists for `addr`.
///
/// **Parameters**
/// - `addr`: Recipient whose pass to query.
///
/// **Returns** Number of yield vaults the pass holder may still create.
view access(all) fun remainingAllowance(addr: Address): UInt64 {
let pass = self.borrowPass(addr: addr)
return pass.remainingAllowance
}

/// Returns the inbox key used to publish and claim a pass capability
/// for the given address.
///
/// **Parameters**
/// - `addr`: Recipient whose inbox entry name to compute.
///
/// **Returns** The inbox key string for the given address.
view access(all) fun inboxName(addr: Address): String {
return "EarlyAccessPass_\(addr.toString())"
}

access(self) fun loadPass(addr: Address): @EarlyAccessPass {
return <- (self.account.storage.load<@EarlyAccessPass>(from: self.passStoragePath(addr: addr)) ?? panic("Pass not found"))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here for loading and below for borrowing: I'd just return an optional and have the caller decide how to handle non-existence.

}

view access(self) fun checkPass(addr: Address): Bool {
return self.account.storage.check<@EarlyAccessPass>(from: self.passStoragePath(addr: addr))
}

view access(self) fun borrowPass(addr: Address): &EarlyAccessPass {
return self.account.storage.borrow<&EarlyAccessPass>(from: self.passStoragePath(addr: addr)) ?? panic("Pass not found")
}

access(self) fun publishPassCapability(addr: Address) {
let capability = self.account.capabilities.storage.issue<&EarlyAccessPass>(self.passStoragePath(addr: addr))
self.account.inbox.publish(capability, name: self.inboxName(addr: addr), recipient: addr)
}

access(self) fun unpublishPassCapability(addr: Address) {
let _ = self.account.inbox.unpublish<&EarlyAccessPass>(self.inboxName(addr: addr))
}

access(self) fun deletePassCapabilities(addr: Address) {
let controllers = self.account.capabilities.storage.getControllers(forPath: self.passStoragePath(addr: addr))
for controller in controllers {
controller.delete()
}
}

view access(self) fun passStoragePath(addr: Address): StoragePath {
return StoragePath(identifier: "FlowYieldVaultsEarlyAccessPass_\(addr.toString())")!
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure, but I think an Address can be used directly as a string template expression:

Suggested change
return StoragePath(identifier: "FlowYieldVaultsEarlyAccessPass_\(addr.toString())")!
return StoragePath(identifier: "FlowYieldVaultsEarlyAccessPass_\(addr)")!

}

init() {
self.adminStoragePath = StoragePath(identifier: "FlowYieldVaultsEarlyAccessAdmin")!
self.passCapabilityStoragePath = StoragePath(identifier: "FlowYieldVaultsEarlyAccessPassCapability")!
self.account.storage.save(<- create Admin(), to: self.adminStoragePath)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import "FlowYieldVaultsEarlyAccess"

access(all) fun main(addr: Address): Bool {
return FlowYieldVaultsEarlyAccess.passExists(addr: addr)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import "FlowYieldVaultsEarlyAccess"

access(all) fun main(addr: Address): UInt64 {
return FlowYieldVaultsEarlyAccess.remainingAllowance(addr: addr)
}
Loading