Skip to content

Commit 13aaf67

Browse files
committed
Merge remote-tracking branch 'origin/main' into nialexsan/testnet-kms
2 parents 663e789 + 68ab3c4 commit 13aaf67

28 files changed

Lines changed: 556 additions & 39 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: 'Build and Push Flow Emulator Image'
2+
3+
on:
4+
workflow_dispatch:
5+
6+
env:
7+
DOCKER_IMAGE_URL: us-west1-docker.pkg.dev/dl-flow-devex-staging/backend/flow-emulator:${{ github.sha }}
8+
9+
jobs:
10+
build-and-push:
11+
runs-on: ubuntu-latest
12+
13+
permissions:
14+
contents: read
15+
id-token: write
16+
17+
steps:
18+
- name: 'Checkout'
19+
uses: 'actions/checkout@v4'
20+
with:
21+
submodules: recursive
22+
token: ${{ secrets.GH_PAT }}
23+
24+
- id: 'auth'
25+
name: 'Authenticate to Google Cloud'
26+
uses: 'google-github-actions/auth@v2'
27+
with:
28+
workload_identity_provider: '${{ vars.BUILDER_WORKLOAD_IDENTITY_PROVIDER }}'
29+
service_account: '${{ vars.BUILDER_SERVICE_ACCOUNT }}'
30+
31+
- name: 'Set up gcloud'
32+
uses: 'google-github-actions/setup-gcloud@v1'
33+
with:
34+
project_id: ${{ vars.GAR_PROJECT_ID }}
35+
36+
- name: 'Build and Push Flow Emulator'
37+
run: |
38+
gcloud auth configure-docker ${{ vars.GAR_REGION }}-docker.pkg.dev
39+
docker build -t "${{ env.DOCKER_IMAGE_URL }}" .
40+
docker push "${{ env.DOCKER_IMAGE_URL }}"

.github/workflows/e2e_tests.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: E2E test
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
e2e-tests:
13+
name: Tidal Yield E2E Test
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
token: ${{ secrets.GH_PAT }}
19+
submodules: recursive
20+
- name: Set up Go
21+
uses: actions/setup-go@v3
22+
with:
23+
go-version: "1.23.x"
24+
- uses: actions/cache@v4
25+
with:
26+
path: ~/go/pkg/mod
27+
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
28+
restore-keys: |
29+
${{ runner.os }}-go-
30+
- name: Install Flow CLI
31+
run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)"
32+
- name: Flow CLI Version
33+
run: flow version
34+
- name: Update PATH
35+
run: echo "/root/.local/bin" >> $GITHUB_PATH
36+
- name: Install dependencies
37+
run: flow deps install --skip-alias --skip-deployments
38+
- name: Run Emulator
39+
run: ./local/run_emulator.sh
40+
- name: Setup Emulator
41+
run: ./local/setup_emulator.sh
42+
- name: Setup Wallets
43+
run: ./local/setup_wallets.sh
44+
- name: Run E2E test
45+
run: ./local/e2e_test.sh

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
1+
.DS_Store
22
# flow
33
*.pkey
44
!local/mock-incrementfi.pkey

Dockerfile

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
FROM debian:stable-slim
2+
3+
ENV FLOW_INSTALL_URL=https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh \
4+
APP_HOME=/app \
5+
SEED_DIR=/seed/state
6+
7+
RUN apt-get update && apt-get install -y --no-install-recommends \
8+
curl ca-certificates jq bash openssl netcat-openbsd git \
9+
&& rm -rf /var/lib/apt/lists/*
10+
11+
# Install Flow CLI
12+
RUN bash -lc 'curl -fsSL "$FLOW_INSTALL_URL" | bash' \
13+
&& mv /root/.local/bin/flow /usr/local/bin/flow \
14+
&& chmod +x /usr/local/bin/flow \
15+
&& flow version
16+
17+
WORKDIR ${APP_HOME}
18+
# Bring in your project files (flow.json, contracts/, scripts/, transactions/, etc.)
19+
COPY . ${APP_HOME}
20+
RUN chmod +x ${APP_HOME}/scripts/*.sh || true
21+
22+
# ---------- PRE-SEED AT BUILD TIME ----------
23+
# Start emulator in background with --persist, wait, seed, then stop.
24+
RUN bash -lc '\
25+
set -euo pipefail; \
26+
mkdir -p "$SEED_DIR"; \
27+
echo "▶ Start emulator (build-time) with --persist to ${SEED_DIR}"; \
28+
flow emulator start --verbose --persist "$SEED_DIR" > /tmp/emulator-build.log 2>&1 & \
29+
EM_PID=$!; \
30+
echo -n "⏳ Waiting for emulator ... "; \
31+
for i in {1..60}; do nc -z 127.0.0.1 3569 && break || { echo -n "."; sleep 1; }; done; echo; \
32+
echo "▶ Seeding"; \
33+
# Your seed scripts can use `--network emulator` exactly like at runtime:
34+
[ -x ./local/setup_wallets.sh ] && ./local/setup_wallets.sh || true; \
35+
[ -x ./local/setup_emulator.sh ] && ./local/setup_emulator.sh || true; \
36+
echo "▶ Stop emulator (build-time)"; \
37+
kill $EM_PID && wait $EM_PID || true \
38+
'
39+
40+
# ---------- RUNTIME ----------
41+
EXPOSE 3569 8080
42+
ENV FLOW_EMULATOR_FLAGS="--verbose --persist /seed/state"
43+
44+
# At runtime we just start the emulator that already contains the baked state.
45+
ENTRYPOINT [ "bash", "-lc", "flow emulator start $FLOW_EMULATOR_FLAGS" ]

cadence/contracts/TidalYield.cdc

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import "Burner"
44
import "ViewResolver"
55
// DeFiActions
66
import "DeFiActions"
7+
import "TidalYieldClosedBeta"
78

89
/// THIS CONTRACT IS A MOCK AND IS NOT INTENDED FOR USE IN PRODUCTION
910
/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@@ -318,7 +319,11 @@ access(all) contract TidalYield {
318319
return self.tides.length
319320
}
320321
/// Creates a new Tide executing the specified Strategy with the provided funds
321-
access(all) fun createTide(strategyType: Type, withVault: @{FungibleToken.Vault}) {
322+
access(all) fun createTide(betaRef: auth(TidalYieldClosedBeta.Beta) &TidalYieldClosedBeta.BetaBadge, strategyType: Type, withVault: @{FungibleToken.Vault}) {
323+
pre {
324+
TidalYieldClosedBeta.validateBeta(self.owner?.address!, betaRef):
325+
"Invalid Beta Ref"
326+
}
322327
let balance = withVault.balance
323328
let type = withVault.getType()
324329
let tide <-create Tide(strategyType: strategyType, withVault: <-withVault)
@@ -332,35 +337,51 @@ access(all) contract TidalYield {
332337
creator: self.owner?.address
333338
)
334339

335-
self.addTide(<-tide)
340+
self.addTide(betaRef: betaRef, <-tide)
336341
}
337342
/// Adds an open Tide to this TideManager resource. This effectively transfers ownership of the newly added
338343
/// Tide to the owner of this TideManager
339-
access(all) fun addTide(_ tide: @Tide) {
344+
access(all) fun addTide(betaRef: auth(TidalYieldClosedBeta.Beta) &TidalYieldClosedBeta.BetaBadge, _ tide: @Tide) {
340345
pre {
341346
self.tides[tide.uniqueID.id] == nil:
342347
"Collision with Tide ID \(tide.uniqueID.id) - a Tide with this ID already exists"
348+
349+
TidalYieldClosedBeta.validateBeta(self.owner?.address!, betaRef):
350+
"Invalid Beta Ref"
343351
}
344352
emit AddedToManager(id: tide.uniqueID.id, owner: self.owner?.address, managerUUID: self.uuid, tokenType: tide.getType().identifier)
345353
self.tides[tide.uniqueID.id] <-! tide
346354
}
347355
/// Deposits additional funds to the specified Tide, reverting if none exists with the provided ID
348-
access(all) fun depositToTide(_ id: UInt64, from: @{FungibleToken.Vault}) {
356+
access(all) fun depositToTide(betaRef: auth(TidalYieldClosedBeta.Beta) &TidalYieldClosedBeta.BetaBadge, _ id: UInt64, from: @{FungibleToken.Vault}) {
349357
pre {
350358
self.tides[id] != nil:
351359
"No Tide with ID \(id) found"
360+
361+
TidalYieldClosedBeta.validateBeta(self.owner?.address!, betaRef):
362+
"Invalid Beta Ref"
352363
}
353364
let tide = (&self.tides[id] as &Tide?)!
354365
tide.deposit(from: <-from)
355366
}
356-
/// Withdraws the specified Tide, reverting if none exists with the provided ID
357-
access(FungibleToken.Withdraw) fun withdrawTide(id: UInt64): @Tide {
367+
access(self) fun _withdrawTide(id: UInt64): @Tide {
358368
pre {
359369
self.tides[id] != nil:
360370
"No Tide with ID \(id) found"
361371
}
362372
return <- self.tides.remove(key: id)!
363373
}
374+
/// Withdraws the specified Tide, reverting if none exists with the provided ID
375+
access(FungibleToken.Withdraw) fun withdrawTide(betaRef: auth(TidalYieldClosedBeta.Beta) &TidalYieldClosedBeta.BetaBadge, id: UInt64): @Tide {
376+
pre {
377+
self.tides[id] != nil:
378+
"No Tide with ID \(id) found"
379+
380+
TidalYieldClosedBeta.validateBeta(self.owner?.address!, betaRef):
381+
"Invalid Beta Ref"
382+
}
383+
return <- self._withdrawTide(id: id)!
384+
}
364385
/// Withdraws funds from the specified Tide in the given amount. The resulting Vault Type will be whatever
365386
/// denomination is supported by the Tide, so callers should examine the Tide to know the resulting Vault to
366387
/// expect
@@ -379,7 +400,7 @@ access(all) contract TidalYield {
379400
self.tides[id] != nil:
380401
"No Tide with ID \(id) found"
381402
}
382-
let tide <- self.withdrawTide(id: id)
403+
let tide <- self._withdrawTide(id: id)
383404
let res <- tide.withdraw(amount: tide.getTideBalance())
384405
Burner.burn(<-tide)
385406
return <-res
@@ -406,7 +427,7 @@ access(all) contract TidalYield {
406427
return <- self._borrowFactory().createStrategy(type, uniqueID: uniqueID, withFunds: <-withFunds)
407428
}
408429
/// Creates a TideManager used to create and manage Tides
409-
access(all) fun createTideManager(): @TideManager {
430+
access(all) fun createTideManager(betaRef: auth(TidalYieldClosedBeta.Beta) &TidalYieldClosedBeta.BetaBadge): @TideManager {
410431
return <-create TideManager()
411432
}
412433
/// Creates a StrategyFactory resource
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
access(all) contract TidalYieldClosedBeta {
2+
3+
access(all) entitlement Admin
4+
access(all) entitlement Beta
5+
6+
access(all) resource BetaBadge {
7+
access(all) let assignedTo: Address
8+
init(_ addr: Address) {
9+
self.assignedTo = addr
10+
}
11+
access(all) view fun getOwner(): Address {
12+
return self.assignedTo
13+
}
14+
}
15+
16+
// --- Paths ---
17+
access(all) let UserBetaCapStoragePath: StoragePath
18+
access(all) let AdminHandleStoragePath: StoragePath
19+
20+
// --- Registry: which capability was issued to which address, and revocation flags ---
21+
access(all) struct AccessInfo {
22+
access(all) let capID: UInt64
23+
access(all) let isRevoked: Bool
24+
25+
init(_ capID: UInt64, _ isRevoked: Bool) {
26+
self.capID = capID
27+
self.isRevoked = isRevoked
28+
}
29+
}
30+
access(all) var issuedCapIDs: {Address: AccessInfo}
31+
32+
// --- Events ---
33+
access(all) event BetaGranted(addr: Address, capID: UInt64)
34+
access(all) event BetaRevoked(addr: Address, capID: UInt64?)
35+
36+
/// Per-user badge storage path (under the *contract/deployer* account)
37+
access(contract) fun _badgePath(_ addr: Address): StoragePath {
38+
return StoragePath(identifier: "TY_BetaBadge_".concat(addr.toString()))!
39+
}
40+
41+
/// Ensure the admin-owned badge exists for the user
42+
access(contract) fun _ensureBadge(_ addr: Address) {
43+
let p = self._badgePath(addr)
44+
if self.account.storage.type(at: p) == nil {
45+
self.account.storage.save(<-create BetaBadge(addr), to: p)
46+
}
47+
}
48+
49+
access(contract) fun _destroyBadge(_ addr: Address) {
50+
let p = self._badgePath(addr)
51+
if let badge <- self.account.storage.load<@BetaBadge>(from: p) {
52+
destroy badge
53+
}
54+
}
55+
56+
/// Issue a capability from the contract/deployer account and record its ID
57+
access(contract) fun _issueBadgeCap(_ addr: Address): Capability<auth(Beta) &BetaBadge> {
58+
let p = self._badgePath(addr)
59+
let cap: Capability<auth(Beta) &BetaBadge> =
60+
self.account.capabilities.storage.issue<auth(Beta) &BetaBadge>(p)
61+
62+
self.issuedCapIDs[addr] = AccessInfo(cap.id, false)
63+
64+
if let ctrl = self.account.capabilities.storage.getController(byCapabilityID: cap.id) {
65+
ctrl.setTag("tidalyield-beta")
66+
}
67+
68+
emit BetaGranted(addr: addr, capID: cap.id)
69+
return cap
70+
}
71+
72+
/// Delete the recorded controller, revoking *all copies* of the capability
73+
access(contract) fun _revokeByAddress(_ addr: Address) {
74+
let info = self.issuedCapIDs[addr] ?? panic("No cap recorded for address")
75+
let ctrl = self.account.capabilities.storage.getController(byCapabilityID: info.capID)
76+
?? panic("Missing controller for recorded cap ID")
77+
ctrl.delete()
78+
self.issuedCapIDs[addr] = AccessInfo(info.capID, true)
79+
self._destroyBadge(addr)
80+
emit BetaRevoked(addr: addr, capID: info.capID)
81+
}
82+
83+
// 2) A small in-account helper resource that performs privileged ops
84+
access(all) resource AdminHandle {
85+
access(Admin) fun grantBeta(addr: Address): Capability<auth(TidalYieldClosedBeta.Beta) &TidalYieldClosedBeta.BetaBadge> {
86+
TidalYieldClosedBeta._ensureBadge(addr)
87+
return TidalYieldClosedBeta._issueBadgeCap(addr)
88+
}
89+
90+
access(Admin) fun revokeByAddress(addr: Address) {
91+
TidalYieldClosedBeta._revokeByAddress(addr)
92+
}
93+
}
94+
95+
/// Read-only check used by any gated entrypoint
96+
access(all) view fun getBetaCapID(_ addr: Address): UInt64? {
97+
if let info = self.issuedCapIDs[addr] {
98+
if info.isRevoked {
99+
assert(info.isRevoked, message: "Beta access revoked")
100+
return nil
101+
}
102+
return info.capID
103+
}
104+
return nil
105+
}
106+
107+
access(all) view fun validateBeta(_ addr: Address?, _ betaRef: auth(Beta) &BetaBadge): Bool {
108+
if (addr == nil) {
109+
assert(addr == nil, message: "Address is required for Beta verification")
110+
return false
111+
}
112+
let recordedID: UInt64? = self.getBetaCapID(addr!);
113+
if recordedID == nil {
114+
assert(recordedID == nil, message: "No Beta access")
115+
return false
116+
}
117+
118+
if betaRef.getOwner() != addr {
119+
assert(betaRef.getOwner() != addr, message: "BetaBadge may only be used by its assigned owner")
120+
return false
121+
}
122+
123+
return true
124+
}
125+
126+
init() {
127+
self.AdminHandleStoragePath = StoragePath(
128+
identifier: "TidalYieldClosedBetaAdmin_\(self.account.address)"
129+
)!
130+
self.UserBetaCapStoragePath = StoragePath(
131+
identifier: "TidalYieldUserBetaCap_\(self.account.address)"
132+
)!
133+
134+
self.issuedCapIDs = {}
135+
136+
// Create and store the admin handle in *this* (deployer) account
137+
self.account.storage.save(<-create AdminHandle(), to: self.AdminHandleStoragePath)
138+
}
139+
}

0 commit comments

Comments
 (0)