Skip to content
Draft
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
26 changes: 26 additions & 0 deletions Dockerfile.mainnet-fork
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
FROM debian:stable-slim

ENV APP_HOME=/app

RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates bash git \
&& rm -rf /var/lib/apt/lists/*

# Install flow CLI pinned to v2.15.0 (matches e2e workflow)
RUN curl -fsSL "https://raw.githubusercontent.com/onflow/flow-cli/v2.15.0/install.sh" | bash \
&& mv /root/.local/bin/flow /usr/local/bin/flow \
&& flow version

WORKDIR ${APP_HOME}
COPY . ${APP_HOME}
RUN chmod +x ${APP_HOME}/local/*.sh

# Pre-install Cadence dependencies at build time (source resolution only;
# mainnet fork state is fetched at container startup, not here)
RUN flow deps install --skip-alias --skip-deployments

EXPOSE 3569 8888

# Runtime: start emulator forked from mainnet.
# The fork state is fetched from access.mainnet.nodes.onflow.org at startup.
ENTRYPOINT ["flow", "emulator", "--fork", "mainnet"]
71 changes: 71 additions & 0 deletions cadence/scripts/diag_pyusd0_owner.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import "EVM"

/// Queries owner(), masterMinter(), and supplyController() on the PYUSD0 ERC20 contract
/// by making view EVM calls via a known COA (admin account).
///
/// Run on fork:
/// flow scripts execute cadence/scripts/diag_pyusd0_owner.cdc --network mainnet-fork
/// Run on mainnet (read-only):
/// flow scripts execute cadence/scripts/diag_pyusd0_owner.cdc --network mainnet

access(all) fun main(): {String: String} {
let pyusd0 = EVM.addressFromString("0x99aF3EeA856556646C98c8B9b2548Fe815240750")
let zero = EVM.Balance(attoflow: 0)

// Borrow the admin COA as a plain reference (no entitlements needed for call in view context).
// The admin account (0xb1d63873c3cc9f79) has a COA at /storage/evm.
let adminAcct = getAccount(0xb1d63873c3cc9f79)
let coa = adminAcct.capabilities
.borrow<&EVM.CadenceOwnedAccount>(/public/evm)

let result: {String: String} = {}

if coa == nil {
result["error"] = "No public COA capability on admin account — run as a transaction instead"
return result
}

// owner() — OpenZeppelin Ownable: selector 0x8da5cb5b
let ownerRes = coa!.call(
to: pyusd0,
data: EVM.encodeABIWithSignature("owner()", []),
gasLimit: 50_000,
value: zero
)
if ownerRes.status == EVM.Status.successful && ownerRes.data.length >= 32 {
let decoded = EVM.decodeABI(types: [Type<EVM.EVMAddress>()], data: ownerRes.data)
result["owner"] = (decoded[0] as! EVM.EVMAddress).toString()
} else {
result["owner"] = "not available (".concat(ownerRes.errorMessage).concat(")")
}

// masterMinter() — Circle FiatToken pattern: selector 0x35d99f35
let mmRes = coa!.call(
to: pyusd0,
data: EVM.encodeABIWithSignature("masterMinter()", []),
gasLimit: 50_000,
value: zero
)
if mmRes.status == EVM.Status.successful && mmRes.data.length >= 32 {
let decoded = EVM.decodeABI(types: [Type<EVM.EVMAddress>()], data: mmRes.data)
result["masterMinter"] = (decoded[0] as! EVM.EVMAddress).toString()
} else {
result["masterMinter"] = "not available (".concat(mmRes.errorMessage).concat(")")
}

// supplyController() — Paxos pattern: selector 0x9720c7fb
let scRes = coa!.call(
to: pyusd0,
data: EVM.encodeABIWithSignature("supplyController()", []),
gasLimit: 50_000,
value: zero
)
if scRes.status == EVM.Status.successful && scRes.data.length >= 32 {
let decoded = EVM.decodeABI(types: [Type<EVM.EVMAddress>()], data: scRes.data)
result["supplyController"] = (decoded[0] as! EVM.EVMAddress).toString()
} else {
result["supplyController"] = "not available (".concat(scRes.errorMessage).concat(")")
}

return result
}
98 changes: 98 additions & 0 deletions cadence/tests/transactions/provision_token_from_flow.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import "FungibleToken"
import "FungibleTokenMetadataViews"
import "FlowToken"
import "EVM"
import "UniswapV3SwapConnectors"
import "FlowEVMBridgeConfig"

/// Swaps native FLOW from the signer's FlowToken vault to a target EVM-bridged token via
/// Uniswap V3, and deposits the result into the signer's Cadence storage.
///
/// This works because WFLOW (the EVM ERC-20 wrapper for FLOW) is registered in
/// FlowEVMBridgeConfig as the EVM representation of FlowToken.Vault. The Swapper
/// bridges FlowToken → WFLOW in EVM, swaps via UniV3, then bridges the output back.
///
/// Example usages (mainnet):
/// FLOW → WETH: tokenInEvm = WFLOW, tokenOutEvm = WETH, fee = 3000
/// FLOW → PYUSD0: tokenInEvm = WFLOW, tokenOutEvm = PYUSD0, fee = 100
///
/// @param factoryAddr UniV3 factory EVM address (hex, with 0x prefix)
/// @param routerAddr UniV3 router EVM address
/// @param quoterAddr UniV3 quoter EVM address
/// @param tokenInEvm WFLOW EVM address (must be registered in FlowEVMBridgeConfig as FlowToken.Vault)
/// @param tokenOutEvm Target token EVM address (must be onboarded to FlowEVMBridge)
/// @param fee UniV3 pool fee tier (e.g. 3000 = 0.3%, 100 = 0.01%)
/// @param flowAmount Amount of FLOW (UFix64) to swap
///
transaction(
factoryAddr: String,
routerAddr: String,
quoterAddr: String,
tokenInEvm: String,
tokenOutEvm: String,
fee: UInt32,
flowAmount: UFix64
) {
prepare(signer: auth(Storage, BorrowValue, IssueStorageCapabilityController, SaveValue, PublishCapability, UnpublishCapability) &Account) {

// Issue a COA capability so the Swapper can bridge tokens via the signer's COA.
let coaCap = signer.capabilities.storage.issue<auth(EVM.Owner) &EVM.CadenceOwnedAccount>(/storage/evm)

let wflowEVM = EVM.addressFromString(tokenInEvm)
let outEVM = EVM.addressFromString(tokenOutEvm)

// FlowToken.Vault is the Cadence representation of WFLOW in FlowEVMBridgeConfig.
let flowVaultType = FlowEVMBridgeConfig.getTypeAssociated(with: wflowEVM)
?? panic("WFLOW EVM address is not registered in FlowEVMBridgeConfig: ".concat(tokenInEvm))
let outVaultType = FlowEVMBridgeConfig.getTypeAssociated(with: outEVM)
?? panic("Target EVM address is not registered in FlowEVMBridgeConfig: ".concat(tokenOutEvm))

let swapper = UniswapV3SwapConnectors.Swapper(
factoryAddress: EVM.addressFromString(factoryAddr),
routerAddress: EVM.addressFromString(routerAddr),
quoterAddress: EVM.addressFromString(quoterAddr),
tokenPath: [wflowEVM, outEVM],
feePath: [fee],
inVault: flowVaultType,
outVault: outVaultType,
coaCapability: coaCap,
uniqueID: nil
)

// Withdraw FLOW from the signer's FlowToken vault.
let flowProvider = signer.storage.borrow<auth(FungibleToken.Withdraw) &FlowToken.Vault>(
from: /storage/flowTokenVault
) ?? panic("Signer has no FlowToken vault at /storage/flowTokenVault")
let inVault <- flowProvider.withdraw(amount: flowAmount)

// Swap: bridges FLOW → WFLOW in EVM, swaps via UniV3, bridges result back to Cadence.
let outVault <- swapper.swap(quote: nil, inVault: <-inVault)
log("Provisioned ".concat(outVault.balance.toString()).concat(" of ").concat(outVaultType.identifier)
.concat(" from ").concat(flowAmount.toString()).concat(" FLOW"))

// Ensure the output vault exists in signer's Cadence storage.
let outCompType = CompositeType(outVaultType.identifier)
?? panic("Cannot construct CompositeType for output vault: ".concat(outVaultType.identifier))
let outContract = getAccount(outCompType.address!).contracts.borrow<&{FungibleToken}>(name: outCompType.contractName!)
?? panic("Cannot borrow FungibleToken contract for output token")
let outVaultData = outContract.resolveContractView(
resourceType: outCompType,
viewType: Type<FungibleTokenMetadataViews.FTVaultData>()
) as? FungibleTokenMetadataViews.FTVaultData
?? panic("Cannot resolve FTVaultData for output token")

if signer.storage.borrow<&{FungibleToken.Vault}>(from: outVaultData.storagePath) == nil {
signer.storage.save(<-outVaultData.createEmptyVault(), to: outVaultData.storagePath)
signer.capabilities.unpublish(outVaultData.receiverPath)
signer.capabilities.unpublish(outVaultData.metadataPath)
let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(outVaultData.storagePath)
let metadataCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(outVaultData.storagePath)
signer.capabilities.publish(receiverCap, at: outVaultData.receiverPath)
signer.capabilities.publish(metadataCap, at: outVaultData.metadataPath)
}

let receiver = signer.storage.borrow<&{FungibleToken.Receiver}>(from: outVaultData.storagePath)
?? panic("Cannot borrow receiver for output vault")
receiver.deposit(from: <-outVault)
}
}
35 changes: 35 additions & 0 deletions cadence/tests/transactions/setup_ft_vault.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import "FungibleToken"
import "FungibleTokenMetadataViews"
import "ViewResolver"

/// Sets up a FungibleToken vault in the signer's storage if not already present,
/// publishing the standard receiver and metadata capabilities.
/// Works with any FT that implements FungibleTokenMetadataViews (including EVMVMBridgedTokens).
///
/// @param contractAddress Address of the token contract (e.g. 0x1e4aa0b87d10b141)
/// @param contractName Name of the token contract

transaction(contractAddress: Address, contractName: String) {
prepare(signer: auth(Storage, Capabilities) &Account) {
let viewResolver = getAccount(contractAddress).contracts.borrow<&{ViewResolver}>(name: contractName)
?? panic("Cannot borrow ViewResolver for ".concat(contractName))

let vaultData = viewResolver.resolveContractView(
resourceType: nil,
viewType: Type<FungibleTokenMetadataViews.FTVaultData>()
) as! FungibleTokenMetadataViews.FTVaultData?
?? panic("Cannot resolve FTVaultData for ".concat(contractName))

if signer.storage.borrow<&{FungibleToken.Vault}>(from: vaultData.storagePath) != nil {
return // already set up
}

signer.storage.save(<-vaultData.createEmptyVault(), to: vaultData.storagePath)
signer.capabilities.unpublish(vaultData.receiverPath)
signer.capabilities.unpublish(vaultData.metadataPath)
let receiverCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath)
let metadataCap = signer.capabilities.storage.issue<&{FungibleToken.Vault}>(vaultData.storagePath)
signer.capabilities.publish(receiverCap, at: vaultData.receiverPath)
signer.capabilities.publish(metadataCap, at: vaultData.metadataPath)
}
}
41 changes: 41 additions & 0 deletions cadence/tests/transactions/transfer_ft_via_vault_data.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import "FungibleToken"
import "FungibleTokenMetadataViews"
import "ViewResolver"

/// Generic FungibleToken transfer that resolves storage/receiver paths via FTVaultData.
/// Works with any FT implementing FungibleTokenMetadataViews (including EVMVMBridgedTokens).
///
/// @param contractAddress Address of the token contract (e.g. 0x1e4aa0b87d10b141)
/// @param contractName Name of the token contract (e.g. EVMVMBridgedToken_99af3eea856556646c98c8b9b2548fe815240750)
/// @param amount Amount to transfer
/// @param to Recipient Cadence address (must already have receiver capability published)

transaction(contractAddress: Address, contractName: String, amount: UFix64, to: Address) {

let sentVault: @{FungibleToken.Vault}
let receiverPath: PublicPath

prepare(signer: auth(BorrowValue) &Account) {
let viewResolver = getAccount(contractAddress).contracts.borrow<&{ViewResolver}>(name: contractName)
?? panic("Cannot borrow ViewResolver for ".concat(contractName))

let vaultData = viewResolver.resolveContractView(
resourceType: nil,
viewType: Type<FungibleTokenMetadataViews.FTVaultData>()
) as! FungibleTokenMetadataViews.FTVaultData?
?? panic("Cannot resolve FTVaultData for ".concat(contractName))

let vaultRef = signer.storage.borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(
from: vaultData.storagePath
) ?? panic("Cannot borrow vault from ".concat(vaultData.storagePath.toString()))

self.sentVault <- vaultRef.withdraw(amount: amount)
self.receiverPath = vaultData.receiverPath
}

execute {
let receiverRef = getAccount(to).capabilities.borrow<&{FungibleToken.Receiver}>(self.receiverPath)
?? panic("Cannot borrow receiver at ".concat(self.receiverPath.toString()))
receiverRef.deposit(from: <-self.sentVault)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import "FlowYieldVaultsClosedBeta"

/// Single-signer variant: admin grants beta access to their own account.
/// Use when the admin is also the test user (avoids multi-sig complexity in shell scripts).
transaction() {
prepare(admin: auth(BorrowValue, Storage) &Account) {
let handle = admin.storage.borrow<auth(FlowYieldVaultsClosedBeta.Admin) &FlowYieldVaultsClosedBeta.AdminHandle>(
from: FlowYieldVaultsClosedBeta.AdminHandleStoragePath
) ?? panic("Missing AdminHandle at AdminHandleStoragePath")

let cap: Capability<auth(FlowYieldVaultsClosedBeta.Beta) &FlowYieldVaultsClosedBeta.BetaBadge> =
handle.grantBeta(addr: admin.address)

let p = FlowYieldVaultsClosedBeta.UserBetaCapStoragePath

if let t = admin.storage.type(at: p) {
if t == Type<Capability<auth(FlowYieldVaultsClosedBeta.Beta) &FlowYieldVaultsClosedBeta.BetaBadge>>() {
let _ = admin.storage.load<Capability<auth(FlowYieldVaultsClosedBeta.Beta) &FlowYieldVaultsClosedBeta.BetaBadge>>(from: p)
} else {
panic("Unexpected type at UserBetaCapStoragePath: ".concat(t.identifier))
}
}
admin.storage.save(cap, to: p)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import "FlowALPv0"
import "BandOracleConnectors"
import "DeFiActions"
import "FungibleTokenConnectors"
import "FungibleToken"

/// Updates the FlowALP pool's price oracle with a larger staleThreshold.
///
/// Use in fork testing environments where Band oracle data may be up to
/// several hours old. The emulator fork uses mainnet state at a fixed
/// height; as real time advances the oracle data becomes stale.
///
/// @param staleThreshold: seconds beyond which oracle data is considered stale
/// Use 86400 (24h) for long-running fork test sessions.
///
/// Must be signed by the FlowALP pool owner (6b00ff876c299c61).
/// In fork mode, signature validation is disabled, so any key can be used.
transaction(staleThreshold: UInt64) {
let pool: auth(FlowALPv0.EGovernance) &FlowALPv0.Pool
let oracle: {DeFiActions.PriceOracle}

prepare(signer: auth(BorrowValue, IssueStorageCapabilityController) &Account) {
self.pool = signer.storage.borrow<auth(FlowALPv0.EGovernance) &FlowALPv0.Pool>(from: FlowALPv0.PoolStoragePath)
?? panic("Could not borrow reference to Pool from \(FlowALPv0.PoolStoragePath)")
let defaultToken = self.pool.getDefaultToken()

let vaultCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(/storage/flowTokenVault)
let feeSource = FungibleTokenConnectors.VaultSource(min: nil, withdrawVault: vaultCap, uniqueID: nil)
self.oracle = BandOracleConnectors.PriceOracle(
unitOfAccount: defaultToken,
staleThreshold: staleThreshold,
feeSource: feeSource,
uniqueID: nil,
)
}

execute {
self.pool.setPriceOracle(self.oracle)
log("FlowALP oracle staleThreshold updated to \(staleThreshold)s")
}
}
25 changes: 14 additions & 11 deletions flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -1034,7 +1034,6 @@
"type": "file",
"location": "local/emulator-account.pkey"
}

},
"testnet-flow-alp-deployer": {
"address": "426f0458ced60037",
Expand All @@ -1043,6 +1042,20 @@
"hashAlgorithm": "SHA2_256",
"resourceID": "projects/dl-flow-devex-staging/locations/us-central1/keyRings/tidal-keyring/cryptoKeys/tidal_admin_pk/cryptoKeyVersions/1"
}
},
"mainnet-fork-flowalp": {
"address": "6b00ff876c299c61",
"key": {
"type": "file",
"location": "local/emulator-account.pkey"
}
},
"mainnet-fork-pyusd0-donor": {
"address": "443472749ebdaac8",
"key": {
"type": "file",
"location": "local/emulator-account.pkey"
}
}
},
"deployments": {
Expand Down Expand Up @@ -1197,16 +1210,6 @@
},
"mainnet-fork": {
"mainnet-fork-admin": [
{
"name": "MockOracle",
"args": [
{
"value": "A.6b00ff876c299c61.MOET.Vault",
"type": "String"
}
]
},
"MockSwapper",
"FlowYieldVaultsSchedulerRegistry",
"FlowYieldVaultsAutoBalancers",
"FlowYieldVaultsSchedulerV1",
Expand Down
2 changes: 1 addition & 1 deletion lib/FlowALP
Submodule FlowALP updated 154 files
Loading