diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..2e369f9 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,6 @@ +# flow +emulator-account.pkey +.env + +# Pay attention to imports directory +!imports \ No newline at end of file diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml new file mode 100644 index 0000000..cb2e68c --- /dev/null +++ b/.github/workflows/e2e_test.yml @@ -0,0 +1,204 @@ +name: Tide Operations CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + setup-and-test: + name: Tide Integration Tests + runs-on: ubuntu-latest + steps: + # === COMMON SETUP === + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PAT }} + submodules: recursive + + - name: Install Flow CLI + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" + + - name: Update PATH + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Verify Flow CLI Installation + run: flow version + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Make scripts executable + run: | + chmod +x ./local/setup_and_run_emulator.sh + chmod +x ./local/deploy_full_stack.sh + + - name: Setup and Run Emulator + run: | + ./local/setup_and_run_emulator.sh & + sleep 80 # Wait for the emulator to be fully up + + - name: Deploy Full Stack + run: | + DEPLOYMENT_OUTPUT=$(./local/deploy_full_stack.sh) + echo "$DEPLOYMENT_OUTPUT" + FLOW_VAULTS_REQUESTS_CONTRACT=$(echo "$DEPLOYMENT_OUTPUT" | grep "FlowVaultsRequests Contract:" | sed 's/.*: //') + echo "CONTRACT_ADDRESS=$FLOW_VAULTS_REQUESTS_CONTRACT" >> $GITHUB_ENV + + # === TEST 1: BASIC TIDE CREATION === + - name: Test 1 - Create Tide (10 FLOW) + run: | + echo "=========================================" + echo "TEST 1: BASIC TIDE CREATION" + echo "=========================================" + forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations \ + --sig "runCreateTide(address)" ${{ env.CONTRACT_ADDRESS }} \ + --rpc-url localhost:8545 \ + --broadcast \ + --legacy + env: + AMOUNT: 10000000000000000000 + + - name: Process Create Request + run: flow transactions send ./cadence/transactions/process_requests.cdc --signer tidal --gas-limit 9999 + + - name: Verify Tide Creation + run: | + echo "=== Verifying Tide Creation ===" + + # Check tide details using the account-level script + TIDE_CHECK=$(flow scripts execute ./cadence/scripts/check_tide_details.cdc 0x045a1763c93006ca) + echo "$TIDE_CHECK" + + # Verify that we have at least one EVM address with tides + if echo "$TIDE_CHECK" | grep -q '"totalEVMAddresses": 1'; then + echo "✅ EVM address registered" + else + echo "❌ No EVM addresses found" + exit 1 + fi + + # Verify that we have at least one tide created + if echo "$TIDE_CHECK" | grep -q '"totalMappedTides": 1'; then + echo "✅ Tide created successfully" + else + echo "❌ No tides found" + exit 1 + fi + + # Verify the specific EVM address has the tide + if echo "$TIDE_CHECK" | grep -q '6813eb9362372eef6200f3b1dbc3f819671cba69'; then + echo "✅ Tide mapped to correct EVM address" + else + echo "❌ EVM address mapping not found" + exit 1 + fi + + echo "✅ Test 1 Passed: Basic tide creation verified" + + # === TEST 2: FULL TIDE LIFECYCLE === + - name: Test 2 - Deposit Additional Funds (20 FLOW) + run: | + echo "=========================================" + echo "TEST 2: FULL TIDE LIFECYCLE" + echo "=========================================" + echo "Step 1: Depositing additional 20 FLOW..." + # Note: Using tide ID 0 based on the event logs from your output + forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations \ + --sig "runDepositToTide(address,uint64)" ${{ env.CONTRACT_ADDRESS }} 0 \ + --rpc-url localhost:8545 \ + --broadcast \ + --legacy + env: + AMOUNT: 20000000000000000000 + + - name: Process Deposit Request + run: flow transactions send ./cadence/transactions/process_requests.cdc --signer tidal --gas-limit 9999 + + - name: Verify Deposit + run: | + echo "Verifying deposit (should still have 1 tide with more balance)..." + + TIDE_CHECK=$(flow scripts execute ./cadence/scripts/check_tide_details.cdc 0x045a1763c93006ca) + echo "$TIDE_CHECK" + + # Should still have 1 tide + if echo "$TIDE_CHECK" | grep -q '"totalMappedTides": 1'; then + echo "✅ Still has 1 tide after deposit" + else + echo "❌ Tide count changed unexpectedly" + exit 1 + fi + + - name: Test 2 - Withdraw Half (15 FLOW) + run: | + echo "Step 2: Withdrawing 15 FLOW..." + forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations \ + --sig "runWithdrawFromTide(address,uint64,uint256)" ${{ env.CONTRACT_ADDRESS }} 0 15000000000000000000 \ + --rpc-url localhost:8545 \ + --broadcast \ + --legacy + + - name: Process Withdraw Request + run: flow transactions send ./cadence/transactions/process_requests.cdc --signer tidal --gas-limit 9999 + + - name: Verify Withdrawal + run: | + echo "Verifying withdrawal (should still have 1 tide with less balance)..." + + TIDE_CHECK=$(flow scripts execute ./cadence/scripts/check_tide_details.cdc 0x045a1763c93006ca) + echo "$TIDE_CHECK" + + # Should still have 1 tide + if echo "$TIDE_CHECK" | grep -q '"totalMappedTides": 1'; then + echo "✅ Still has 1 tide after withdrawal" + else + echo "❌ Tide count changed unexpectedly" + exit 1 + fi + + - name: Test 2 - Close Tide + run: | + echo "Step 3: Closing tide (withdrawing remaining funds)..." + forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations \ + --sig "runCloseTide(address,uint64)" ${{ env.CONTRACT_ADDRESS }} 0 \ + --rpc-url localhost:8545 \ + --broadcast \ + --legacy + + - name: Process Close Request + run: flow transactions send ./cadence/transactions/process_requests.cdc --signer tidal --gas-limit 9999 + + - name: Verify Tide Closed + run: | + echo "Verifying tide was closed..." + + TIDE_CHECK=$(flow scripts execute ./cadence/scripts/check_tide_details.cdc 0x045a1763c93006ca) + echo "$TIDE_CHECK" + + # After closing, should have 0 tides or the tide should be marked as closed + if echo "$TIDE_CHECK" | grep -q '"totalMappedTides": 0'; then + echo "✅ Tide successfully closed and removed" + elif echo "$TIDE_CHECK" | grep -q '"totalEVMAddresses": 0'; then + echo "✅ No more active tides for EVM addresses" + else + echo "⚠️ Tide may still exist but should be in closed state" + # Don't fail here as the close transaction succeeded + fi + + echo "✅ Test 2 Passed: Full tide lifecycle completed" + + # === FINAL SUMMARY === + - name: Test Summary + run: | + echo "=========================================" + echo "ALL INTEGRATION TESTS PASSED" + echo "=========================================" + echo "✅ Test 1: Basic Tide Creation - PASSED" + echo "✅ Test 2: Full Tide Lifecycle - PASSED" + echo "=========================================" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d471d8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +imports +db + +.env +.secrets + +# Cache files +cache/ +broadcast/ + +# Build output +out/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 4df8d73..b131ed6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ -[submodule "lib/tidal-sc"] - path = lib/tidal-sc - url = https://github.com/onflow/tidal-sc.git +[submodule "lib/flow-vaults-sc"] + path = lib/flow-vaults-sc + url = https://github.com/onflow/FlowVaults-sc.git +[submodule "solidity/lib/forge-std"] + path = solidity/lib/forge-std + url = https://github.com/foundry-rs/forge-std \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cd573be --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "wake.compiler.solc.remappings": [] +} \ No newline at end of file diff --git a/FLOW_VAULTS_EVM_BRIDGE_DESIGN.md b/FLOW_VAULTS_EVM_BRIDGE_DESIGN.md new file mode 100644 index 0000000..a4156b7 --- /dev/null +++ b/FLOW_VAULTS_EVM_BRIDGE_DESIGN.md @@ -0,0 +1,1145 @@ +# Flow Vaults Cross-VM Bridge: EVM ↔ Cadence Design Document + +## Executive Summary + +This document outlines the architecture for enabling Flow EVM users to interact with Flow Vaults's Cadence-based yield protocol through a scheduled cross-VM bridge pattern. + +**Key Innovation**: EVM users deposit funds and submit requests to a Solidity contract, which are periodically processed by a Cadence worker that bridges funds and manages Tide positions on their behalf. + +--- + +## Architecture Overview + +### Components + +#### 1. **FlowVaultsRequests** (Solidity - Flow EVM) +- **Purpose**: Request queue and fund escrow for EVM users +- **Location**: Flow EVM +- **Responsibilities**: + - Accept user requests (CREATE_TIDE, DEPOSIT, WITHDRAW, CLOSE) + - Escrow native $FLOW and ERC-20 tokens + - Track per-user request queues + - Track escrowed funds awaiting processing (not actual Tide balances) + - Only allow fund withdrawals by the authorized COA + +#### 2. **FlowVaultsEVM** (Cadence) +- **Purpose**: Scheduled processor that executes EVM user requests on Cadence +- **Location**: Flow Cadence +- **Responsibilities**: + - Poll FlowVaultsRequests contract at regular intervals (e.g., every 2 minutes or 1 hour) + - Own and control the COA resource + - Bridge funds between EVM and Cadence + - Create and manage Tide positions tagged by EVM user address + - Update request statuses and user balances in FlowVaultsRequests + - Emit events for traceability + +#### 3. **COA (Cadence Owned Account)** +- **Purpose**: Bridge account controlled by FlowVaultsEVM +- **Ownership**: FlowVaultsEVM holds the resource +- **Responsibilities**: + - Withdraw funds from FlowVaultsRequests (via Solidity `onlyAuthorizedCOA` modifier) + - Bridge funds from EVM to Cadence + - Bridge funds from Cadence back to EVM for withdrawals (directly and atomically to user's EVM address) + + +![Flow Vaults EVM Bridge Design](./create_tide.png) + +*This diagram illustrates the complete flow for creating a new position (tide), from the user's initial request in the EVM environment through to the creation of the tide in Cadence.* + +--- + +## Data Structures + +### FlowVaultsRequests (Solidity) + +```solidity +contract FlowVaultsRequests { + // ============================================ + // Constants + // ============================================ + + /// @notice Special address representing native $FLOW (similar to 1inch approach) + /// @dev Using recognizable pattern instead of address(0) for clarity + address public constant NATIVE_FLOW = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; + + // ============================================ + // Enums + // ============================================ + + enum RequestType { + CREATE_TIDE, + DEPOSIT_TO_TIDE, + WITHDRAW_FROM_TIDE, + CLOSE_TIDE + } + + enum RequestStatus { + PENDING, + PROCESSING, + COMPLETED, + FAILED + } + + // ============================================ + // Structs + // ============================================ + + struct Request { + uint256 id; + address user; + RequestType requestType; + RequestStatus status; + address tokenAddress; + uint256 amount; + uint64 tideId; // Only used for DEPOSIT/WITHDRAW/CLOSE + uint256 timestamp; + string message; // Error/status message (especially for failures) + } + + // ============================================ + // State Variables + // ============================================ + + /// @notice Auto-incrementing request ID counter + uint256 private _requestIdCounter; + + /// @notice Authorized COA address (controlled by FlowVaultsEVM) + address public authorizedCOA; + + /// @notice Owner of the contract (for admin functions) + address public owner; + + /// @notice User request history: user address => array of requests + mapping(address => Request[]) public userRequests; + + /// @notice Pending user balances: user address => token address => balance + /// @dev Tracks escrowed funds in the EVM contract awaiting processing + /// Does NOT track actual Tide balances on Cadence side + mapping(address => mapping(address => uint256)) public pendingUserBalances; + + /// @notice Pending requests for efficient worker processing + mapping(uint256 => Request) public pendingRequests; + uint256[] public pendingRequestIds; + + // ============================================ + // Events + // ============================================ + + event RequestCreated( + uint256 indexed requestId, + address indexed user, + RequestType requestType, + address indexed tokenAddress, + uint256 amount, + uint64 tideId + ); + + event RequestProcessed( + uint256 indexed requestId, + RequestStatus status, + uint64 tideId, + string message + ); + + event BalanceUpdated( + address indexed user, + address indexed tokenAddress, + uint256 newBalance + ); + + event FundsWithdrawn( + address indexed to, + address indexed tokenAddress, + uint256 amount + ); + + event AuthorizedCOAUpdated(address indexed oldCOA, address indexed newCOA); + + // ============================================ + // Modifiers + // ============================================ + + modifier onlyAuthorizedCOA() { + require( + msg.sender == authorizedCOA, + "FlowVaultsRequests: caller is not authorized COA" + ); + _; + } + + modifier onlyOwner() { + require(msg.sender == owner, "FlowVaultsRequests: caller is not owner"); + _; + } + + // ============================================ + // Key Functions + // ============================================ + + /// @notice Create a new Tide (deposit funds to create position) + function createTide(address tokenAddress, uint256 amount) external payable returns (uint256); + + /// @notice Withdraw from existing Tide + function withdrawFromTide(uint64 tideId, uint256 amount) external returns (uint256); + + /// @notice Close Tide and withdraw all funds + function closeTide(uint64 tideId) external returns (uint256); + + /// @notice Withdraw funds from contract (only authorized COA) + function withdrawFunds(address tokenAddress, uint256 amount) external onlyAuthorizedCOA; + + /// @notice Update request status (only authorized COA) + function updateRequestStatus(uint256 requestId, RequestStatus status, uint64 tideId, string memory message) external onlyAuthorizedCOA; + + /// @notice Update user balance (only authorized COA) + function updateUserBalance(address user, address tokenAddress, uint256 newBalance) external onlyAuthorizedCOA; + + /// @notice Get pending requests unpacked (for Cadence decoding) + function getPendingRequestsUnpacked() external view returns ( + uint256[] memory ids, + address[] memory users, + uint8[] memory requestTypes, + uint8[] memory statuses, + address[] memory tokenAddresses, + uint256[] memory amounts, + uint64[] memory tideIds, + uint256[] memory timestamps, + string[] memory messages + ); + + /// @notice Helper function to check if token is native FLOW + function isNativeFlow(address token) public pure returns (bool) { + return token == NATIVE_FLOW; + } +} +``` + +### FlowVaultsEVM (Cadence) + +```cadence +access(all) contract FlowVaultsEVM { + + // ======================================== + // Paths + // ======================================== + + access(all) let WorkerStoragePath: StoragePath + access(all) let WorkerPublicPath: PublicPath + access(all) let AdminStoragePath: StoragePath + + // ======================================== + // State + // ======================================== + + /// Mapping of EVM addresses (as hex strings) to their Tide IDs + /// Example: "0x1234..." => [1, 5, 12] + access(all) let tidesByEVMAddress: {String: [UInt64]} + + /// FlowVaultsRequests contract address on EVM side + /// Can only be set by Admin + access(all) var flowVaultsRequestsAddress: EVM.EVMAddress? + + // ======================================== + // Events + // ======================================== + + access(all) event WorkerInitialized(coaAddress: String) + access(all) event FlowVaultsRequestsAddressSet(address: String) + access(all) event RequestsProcessed(count: Int, successful: Int, failed: Int) + access(all) event TideCreatedForEVMUser(evmAddress: String, tideId: UInt64, amount: UFix64) + access(all) event TideClosedForEVMUser(evmAddress: String, tideId: UInt64, amountReturned: UFix64) + access(all) event RequestFailed(requestId: UInt256, reason: String) + + // ======================================== + // Structs + // ======================================== + + /// Represents a request from EVM side + access(all) struct EVMRequest { + access(all) let id: UInt256 + access(all) let user: EVM.EVMAddress + access(all) let requestType: UInt8 + access(all) let status: UInt8 + access(all) let tokenAddress: EVM.EVMAddress + access(all) let amount: UInt256 + access(all) let tideId: UInt64 + access(all) let timestamp: UInt256 + access(all) let message: String + + init( + id: UInt256, + user: EVM.EVMAddress, + requestType: UInt8, + status: UInt8, + tokenAddress: EVM.EVMAddress, + amount: UInt256, + tideId: UInt64, + timestamp: UInt256, + message: String + ) { + self.id = id + self.user = user + self.requestType = requestType + self.status = status + self.tokenAddress = tokenAddress + self.amount = amount + self.tideId = tideId + self.timestamp = timestamp + self.message = message + } + } + + access(all) struct ProcessResult { + access(all) let success: Bool + access(all) let tideId: UInt64 + access(all) let message: String + + init(success: Bool, tideId: UInt64, message: String) { + self.success = success + self.tideId = tideId + self.message = message + } + } + + // ======================================== + // Admin Resource + // ======================================== + + /// Admin capability for managing the bridge + /// Only the contract account should hold this + access(all) resource Admin { + access(all) fun setFlowVaultsRequestsAddress(_ address: EVM.EVMAddress) + + /// Create a new Worker with a capability instead of reference + access(all) fun createWorker( + coa: @EVM.CadenceOwnedAccount, + betaBadgeCap: Capability + ): @Worker + } + + // ======================================== + // Worker Resource + // ======================================== + + access(all) resource Worker { + /// COA resource for cross-VM operations + access(self) let coa: @EVM.CadenceOwnedAccount + + /// TideManager to hold Tides for EVM users + access(self) let tideManager: @FlowVaults.TideManager + + /// Capability to beta badge (instead of reference) + access(self) let betaBadgeCap: Capability + + /// Get COA's EVM address as string + access(all) fun getCOAAddressString(): String + + /// Process all pending requests from FlowVaultsRequests contract + access(all) fun processRequests() + + /// Process CREATE_TIDE request + access(self) fun processCreateTide(_ request: EVMRequest): ProcessResult + + /// Process CLOSE_TIDE request + access(self) fun processCloseTide(_ request: EVMRequest): ProcessResult + + /// Withdraw funds from FlowVaultsRequests contract via COA + access(self) fun withdrawFundsFromEVM(amount: UFix64): @{FungibleToken.Vault} + + /// Bridge funds from Cadence back to EVM user (atomic) + access(self) fun bridgeFundsToEVMUser(vault: @{FungibleToken.Vault}, recipient: EVM.EVMAddress) + + /// Update request status in FlowVaultsRequests + access(self) fun updateRequestStatus(requestId: UInt256, status: UInt8, tideId: UInt64, message: String) + + /// Update user balance in FlowVaultsRequests + access(self) fun updateUserBalance(user: EVM.EVMAddress, tokenAddress: EVM.EVMAddress, newBalance: UInt256) + + /// Get pending requests from FlowVaultsRequests contract + access(all) fun getPendingRequestsFromEVM(): [EVMRequest] + } + + // ======================================== + // Public Functions + // ======================================== + + /// Get Tide IDs for an EVM address + access(all) fun getTideIDsForEVMAddress(_ evmAddress: String): [UInt64] + + /// Get FlowVaultsRequests address (read-only) + access(all) fun getFlowVaultsRequestsAddress(): EVM.EVMAddress? + + /// Helper: Convert UInt256 (18 decimals) to UFix64 (8 decimals) + access(self) fun ufix64FromUInt256(_ value: UInt256): UFix64 + + /// Helper: Convert UFix64 (8 decimals) to UInt256 (18 decimals) + access(self) fun uint256FromUFix64(_ value: UFix64): UInt256 +} +``` + +--- + +## Request Flow Diagrams + +### 1. CREATE_TIDE Flow + +``` +EVM User A FlowVaultsRequests FlowVaultsEVM FlowVaults FlowScheduler + | | | | | + | | | | | + | 1. createRequest() | | | | + |--(NATIVE_FLOW, 1.0)----->| | | | + | + 1.0 $FLOW | | | | + | | | | | + | 2. Store request | | | | + | in userRequests | | | | + | mapping | | | | + | | | | | + | 3. Update userBalances | | | | + | [userA][NATIVE_FLOW] | | | | + | += 1.0 | | | | + | | | | | + | 4. Add to pending queue | | | | + | pendingRequestIds[] | | | | + | | | | | + |<-----RequestCreated------| | | | + | event (id=1) | | | | + | | | | | + | | |<-- 5. SCHEDULED TXN ---| | + | | | Every X minutes | | + | | | (e.g. 2min/1hr) | | + | | | | | + | | 6. getPendingRequests()| | | + | |<----------------------| | | + | | | | | + | |---[Request{id=1}]---->| | | + | | | | | + | | | 7. Mark PROCESSING | | + | |<--updateRequestStatus-| | | + | | (id=1, PROCESSING) | | | + | | | | | + | | 8. withdrawFunds() | | | + | |<--(NATIVE_FLOW, 1.0)--| | | + | | via COA (authorized)| | | + | | | | | + | |---1.0 FLOW transfer-->| | | + | | to COA EVM address | | | + | | | | | + | | | 9. COA.withdraw() | | + | | | EVM → Cadence | | + | | | | | + | | |<--@FlowToken.Vault---| | + | | | (1.0 FLOW) | | + | | | | | + | | | 10. createTide() | | + | | |---(strategyType)---->| | + | | | + vault (1.0 FLOW) | | + | | | | | + | | | | 11. Create Tide | + | | | | resource | + | | | | with strategy | + | | | | | + | | | 12. Store Tide | | + | | |<--Tide (id=42)-------| | + | | | | | + | | | 13. Map in storage: | | + | | | tidesByEVMAddr | | + | | | [userA] = [42] | | + | | | | | + | | 14. Update balance | | | + | |<--updateUserBalance---| | | + | | (userA, NATIVE_FLOW,| | | + | | newBalance=0) | | | + | | | | | + |<--BalanceUpdated---------| | | | + | event (userA, 0) | | | | + | | | | | + | | 15. Mark COMPLETED | | | + | |<--updateRequestStatus-| | | + | | (id=1, COMPLETED, | | | + | | tideId=42) | | | + | | | | | + | | 16. Remove from | | | + | | pending queue | | | + | | | | | + |<--RequestProcessed-------| | | | + | event (id=1, | | | | + | COMPLETED, | | | | + | tideId=42) | | | | + | | | | | + | ✅ User can now query | | | | + | their Tide ID: 42 | | | | +``` + +### 2. WITHDRAW_FROM_TIDE Flow + +``` +EVM User A FlowVaultsRequests FlowVaultsEVM FlowVaults FlowScheduler + | | | | | + | 1. createRequest() | | | | + |--(WITHDRAW, 0.5, tid=42)-| | | | + | no FLOW sent | | | | + | | | | | + | 2. Store request | | | | + | requestType=WITHDRAW | | | | + | tideId=42, amount=0.5 | | | | + | | | | | + | 3. Add to pending queue | | | | + | | | | | + |<-----RequestCreated------| | | | + | event (id=2) | | | | + | | | | | + | | |<-- 4. SCHEDULED TXN ---| | + | | | (next interval) | | + | | | | | + | | 5. getPendingRequests()| | | + | |<----------------------| | | + | |---[Request{id=2}]---->| | | + | | | | | + | | | 6. Validate request | | + | | | Check tideId=42 | | + | | | exists for userA | | + | | | | | + | | 7. Mark PROCESSING | | | + | |<--updateRequestStatus-| | | + | | (id=2, PROCESSING) | | | + | | | | | + | | | 8. withdrawFromTide()| | + | | |---(tideId=42, 0.5)-->| | + | | | | | + | | | | 9. Withdraw from | + | | | | Tide resource | + | | | | Update balance | + | | | | | + | | |<--@FlowToken.Vault---| | + | | | (0.5 FLOW) | | + | | | | | + | | | 10. Get user's EVM | | + | | | address from | | + | | | request | | + | | | | | + | | | 11. recipientAddress | | + | | | .deposit() | | + | | | Cadence → EVM | | + | | | (ATOMIC!) | | + | | | | | + |<----0.5 FLOW received----| | | | + | directly to wallet | | | | + | | | | | + | | 12. Optional: Update | | | + | | accounting | | | + | |<--updateUserBalance---| | | + | | (userA, NATIVE_FLOW,| | | + | | decreased) | | | + | | | | | + |<--BalanceUpdated---------| | | | + | event (if needed) | | | | + | | | | | + | | 13. Mark COMPLETED | | | + | |<--updateRequestStatus-| | | + | | (id=2, COMPLETED) | | | + | | | | | + | | 14. Remove from | | | + | | pending queue | | | + | | | | | + |<--RequestProcessed-------| | | | + | event (id=2, | | | | + | COMPLETED) | | | | + | | | | | + | ✅ User received 0.5 FLOW| | | | + | in their EVM wallet | | | | +``` + +--- + +## Flow Scheduled Transactions Integration + +### Overview + +The FlowVaultsEVM uses **Flow's scheduled transaction capability** to periodically process pending requests from the EVM side. This is a key architectural component that enables the asynchronous bridge pattern. + +### Scheduling Mechanism + +The scheduling mechanism uses Flow's built-in scheduled transaction system with a **handler pattern** that stores a capability to the Worker resource. + +#### 1. FlowVaultsTransactionHandler Contract + +First, create a handler contract that implements the `FlowTransactionScheduler.TransactionHandler` interface: + +```cadence +import "FlowTransactionScheduler" +import "FlowVaultsEVM" + +access(all) contract FlowVaultsTransactionHandler { + + /// Handler resource that implements the Scheduled Transaction interface + access(all) resource Handler: FlowTransactionScheduler.TransactionHandler { + + /// Capability to the FlowVaultsEVM Worker + /// This is stored in the handler to avoid direct storage borrowing + access(self) let workerCap: Capability<&FlowVaultsEVM.Worker> + + init(workerCap: Capability<&FlowVaultsEVM.Worker>) { + self.workerCap = workerCap + } + + /// Called automatically by the scheduler + access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) { + let worker = self.workerCap.borrow() + ?? panic("Could not borrow Worker capability") + + // Execute the actual processing logic + worker.processRequests() + + log("FlowVaultsEVM scheduled transaction executed (id: ".concat(id.toString()).concat(")")) + } + + access(all) view fun getViews(): [Type] { + return [Type(), Type()] + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + switch view { + case Type(): + return /storage/FlowVaultsTransactionHandler + case Type(): + return /public/FlowVaultsTransactionHandler + default: + return nil + } + } + } + + /// Factory for the handler resource + access(all) fun createHandler(workerCap: Capability<&FlowVaultsEVM.Worker>): @Handler { + return <- create Handler(workerCap: workerCap) + } +} +``` + +#### 2. Initialize Handler (One-time Setup) + +```cadence +import "FlowVaultsTransactionHandler" +import "FlowTransactionScheduler" +import "FlowVaultsEVM" + +transaction() { + prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, SaveValue, PublishCapability) &Account) { + // Create a capability to the Worker + let workerCap = signer.capabilities.storage + .issue<&FlowVaultsEVM.Worker>(FlowVaultsEVM.WorkerStoragePath) + + // Create and save the handler with the worker capability + if signer.storage.borrow<&AnyResource>(from: /storage/FlowVaultsTransactionHandler) == nil { + let handler <- FlowVaultsTransactionHandler.createHandler(workerCap: workerCap) + signer.storage.save(<-handler, to: /storage/FlowVaultsTransactionHandler) + } + + // Issue an entitled capability for the scheduler to call executeTransaction + let _ = signer.capabilities.storage + .issue(/storage/FlowVaultsTransactionHandler) + + // Issue a public capability for general access + let publicCap = signer.capabilities.storage + .issue<&{FlowTransactionScheduler.TransactionHandler}>(/storage/FlowVaultsTransactionHandler) + signer.capabilities.publish(publicCap, at: /public/FlowVaultsTransactionHandler) + } +} +``` + +#### 3. Schedule the Recurring Transaction + +```cadence +import "FlowTransactionScheduler" +import "FlowTransactionSchedulerUtils" +import "FlowToken" +import "FungibleToken" + +/// Schedule FlowVaultsEVM request processing at a future timestamp +transaction( + delaySeconds: UFix64, // e.g., 120.0 for 2 minutes + priority: UInt8, // 0=High, 1=Medium, 2=Low + executionEffort: UInt64 // Must be >= 10, recommend 1000+ +) { + prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, SaveValue, GetStorageCapabilityController, PublishCapability) &Account) { + let future = getCurrentBlock().timestamp + delaySeconds + + let pr = priority == 0 + ? FlowTransactionScheduler.Priority.High + : priority == 1 + ? FlowTransactionScheduler.Priority.Medium + : FlowTransactionScheduler.Priority.Low + + // Get the entitled handler capability + var handlerCap: Capability? = nil + let controllers = signer.capabilities.storage.getControllers(forPath: /storage/FlowVaultsTransactionHandler) + + for controller in controllers { + if let cap = controller.capability as? Capability { + handlerCap = cap + break + } + } + + // Initialize manager if not present + if signer.storage.borrow<&AnyResource>(from: FlowTransactionSchedulerUtils.managerStoragePath) == nil { + let manager <- FlowTransactionSchedulerUtils.createManager() + signer.storage.save(<-manager, to: FlowTransactionSchedulerUtils.managerStoragePath) + + let managerCapPublic = signer.capabilities.storage + .issue<&{FlowTransactionSchedulerUtils.Manager}>(FlowTransactionSchedulerUtils.managerStoragePath) + signer.capabilities.publish(managerCapPublic, at: FlowTransactionSchedulerUtils.managerPublicPath) + } + + // Borrow the manager + let manager = signer.storage + .borrow( + from: FlowTransactionSchedulerUtils.managerStoragePath + ) ?? panic("Could not borrow Manager") + + // Estimate and withdraw fees + let vaultRef = signer.storage + .borrow(from: /storage/flowTokenVault) + ?? panic("Missing FlowToken vault") + + let est = FlowTransactionScheduler.estimate( + data: nil, + timestamp: future, + priority: pr, + executionEffort: executionEffort + ) + + let fees <- vaultRef.withdraw(amount: est.flowFee ?? 0.0) as! @FlowToken.Vault + + // Schedule the transaction + let transactionId = manager.schedule( + handlerCap: handlerCap ?? panic("Could not get handler capability"), + data: nil, + timestamp: future, + priority: pr, + executionEffort: executionEffort, + fees: <-fees + ) + + log("Scheduled FlowVaultsEVM processing (id: " + .concat(transactionId.toString()) + .concat(") at ") + .concat(future.toString())) + } +} +``` + +**Key Architecture Points:** +- The **Handler** stores a capability to the Worker (not a direct reference) +- The **scheduled transaction** calls the Handler through its entitled capability +- The **Handler** uses its stored Worker capability to execute `processRequests()` +- This pattern enables proper separation of concerns and follows Flow best practices + +--- + +## Smart Dynamic Scheduling for Scale + +### Problem Statement + +As discussed with Joshua, processing all requests in a single scheduled transaction is problematic: +- Each EVM call consumes significant gas +- Gas limits restrict the number of requests processable per transaction +- High user volume could lead to request backlogs +- **We should not assume unlimited capacity** - must design with batch limits from day one + +### Solution: Self-Scheduling with Adaptive Frequency + +Instead of assuming unlimited capacity, the system uses a **self-scheduling pattern** where: +1. Each execution processes a **fixed maximum** number of requests (e.g., 10, determined by gas benchmarking) +2. After processing, the handler **checks remaining queue depth** +3. The handler **schedules its own next execution** with adaptive timing based on load + +### Implementation + +#### 1. Batch Processing Constant + +```cadence +access(all) contract FlowVaultsEVM { + /// Maximum requests to process per transaction (determined by gas benchmarking) + access(all) let MAX_REQUESTS_PER_TX: Int + + init() { + // ... other initialization ... + self.MAX_REQUESTS_PER_TX = 10 // Set based on testing + } +} +``` + +#### 2. Modified Worker.processRequests() + +```cadence +access(all) fun processRequests() { + pre { + FlowVaultsEVM.flowVaultsRequestsAddress != nil: "FlowVaultsRequests address not set" + } + + // 1. Get pending requests from FlowVaultsRequests + let allRequests = self.getPendingRequestsFromEVM() + + // 2. Process only up to MAX_REQUESTS_PER_TX + let batchSize = allRequests.length < FlowVaultsEVM.MAX_REQUESTS_PER_TX + ? allRequests.length + : FlowVaultsEVM.MAX_REQUESTS_PER_TX + + var successCount = 0 + var failCount = 0 + var i = 0 + + while i < batchSize { + let success = self.processRequestSafely(allRequests[i]) + if success { + successCount = successCount + 1 + } else { + failCount = failCount + 1 + } + i = i + 1 + } + + emit RequestsProcessed(count: batchSize, successful: successCount, failed: failCount) + + // 3. Schedule next execution based on remaining queue depth + let remainingRequests = allRequests.length - batchSize + self.scheduleNextExecution(remainingCount: remainingRequests) +} +``` + +#### 3. Adaptive Scheduling Logic + +```cadence +access(self) fun scheduleNextExecution(remainingCount: Int) { + // Determine delay based on queue depth + let delay: UFix64 + + if remainingCount > 50 { + // High load: process again in 10 seconds + delay = 10.0 + } else if remainingCount > 0 { + // Normal load: process again in 2 minutes + delay = 120.0 + } else { + // Empty queue: check again in 1 hour + delay = 3600.0 + } + + // Calculate future timestamp + let nextRunTime = getCurrentBlock().timestamp + delay + + // Get scheduler manager and fees + let manager = // ... borrow manager from contract account + let vaultRef = // ... borrow vault from contract account + + // Estimate fees + let est = FlowTransactionScheduler.estimate( + data: nil, + timestamp: nextRunTime, + priority: FlowTransactionScheduler.Priority.Medium, + executionEffort: 5000 + ) + + let fees <- vaultRef.withdraw(amount: est.flowFee ?? 0.0) + + // Schedule next run + let transactionId = manager.schedule( + handlerCap: self.getHandlerCapability(), + data: nil, + timestamp: nextRunTime, + priority: FlowTransactionScheduler.Priority.Medium, + executionEffort: 5000, + fees: <-fees + ) + + log("Scheduled next execution (id: " + .concat(transactionId.toString()) + .concat(") for ") + .concat(nextRunTime.toString()) + .concat(" with ") + .concat(remainingCount.toString()) + .concat(" requests remaining")) +} +``` + +#### 4. Get Pending Count (New Function) + +Add to FlowVaultsRequests Solidity contract: + +```solidity +/// @notice Get count of pending requests (gas-efficient) +function getPendingRequestCount() external view returns (uint256) { + return pendingRequestIds.length; +} +``` + +The Worker can call this for lightweight queue depth checks without fetching all request data. + +### Scaling Characteristics + +| Queue Depth | Delay | Processing Rate | Use Case | +|-------------|-------|-----------------|----------| +| 0 requests | 1 hour | Minimal overhead | Low activity periods | +| 1-50 requests | 2 minutes | Normal processing | Regular usage | +| 50+ requests | 10 seconds | High-throughput mode | Peak demand | + +### Benefits + +1. **Gas Efficiency**: Each transaction stays well under gas limits +2. **Fully Autonomous**: No off-chain monitoring needed - system scales itself +3. **Adaptive**: Automatically scales processing frequency with demand +4. **Cost-Effective**: Reduces scheduled transaction fees during low usage +5. **Predictable**: Fixed batch size makes gas usage predictable +6. **No Assumption of Unlimited Capacity**: Built with scaling constraints from day one + +### Trade-offs + +- **Processing Delay**: Users may wait up to MAX_DELAY (e.g., 1 hour) for processing during low activity +- **Complexity**: More sophisticated than simple fixed-interval scheduling + +### Failover & Reliability + +**What if scheduled transaction fails?** + +1. **Automatic Retry:** Flow will retry failed scheduled transactions +2. **Circuit Breaker:** Pause scheduling if failure rate > threshold +3. **Manual Intervention:** Admin can trigger manual processing +4. **Fallback Queue:** Requests remain in EVM contract until processed + +```cadence +access(all) fun processRequests() { + // Check circuit breaker + if self.isCircuitBroken() { + emit ScheduledExecutionSkipped(reason: "Circuit breaker active") + return + } + + // Attempt processing with error recovery + self.processWithErrorRecovery() +} +``` + +### Failure Event Emission + +When a request fails during processing, the system emits detailed events for monitoring and debugging: + +```cadence +access(all) event RequestFailed( + requestId: UInt256, + reason: String +) +``` + +--- + +### 1. **Request Queue Pattern** +- **Decision**: Use a pull-based model where FlowVaultsEVM polls for requests +- **Rationale**: + - fully on-chain no off-chain event listeners + - Worker can process multiple requests in one transaction (if gas < 9999, need some tests to estimate) + +### 2. **Fund Escrow in FlowVaultsRequests** +- **Decision**: Funds remain in FlowVaultsRequests until processed +- **Rationale**: + - Security: Only authorized COA can withdraw + - Transparency: Easy to audit locked funds + - Rollback safety: Failed requests don't lose funds + +### 3. **Separated State Management Across VMs** +- **Decision**: Each VM maintains its own relevant state independently + - **EVM (FlowVaultsRequests)**: Tracks escrowed funds awaiting processing via `userBalances` + - **Cadence (FlowVaultsEVM)**: Holds actual Tide positions and real-time balances +- **Rationale**: + - The Solidity contract cannot track real-time Tide balances from Cadence + - Maintaining duplicate state across VMs is neither necessary nor feasible given the asynchronous bridge design + - Users query each side independently: + - EVM queries show funds in escrow (pending processing) + - Cadence queries show actual Tide positions and current balances + - Simpler architecture without cross-VM synchronization complexity + +### 4. **Tide Storage by EVM Address** +- **Decision**: Store Tides in FlowVaultsEVM tagged by EVM address string +- **Rationale**: + - Clear ownership mapping + - Efficient lookups for subsequent operations + - Supports multiple Tides per user + +### 5. **Native $FLOW vs ERC-20 Tokens** +- **Decision**: Use a constant address `NATIVE_FLOW` for native token +- **Rationale**: + - Follows DeFi best practices (similar to 1inch, Uniswap, etc.) + - Address pattern: `0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF` (recognizable) + - Different transfer mechanisms (native value transfer vs ERC-20 transferFrom) + - Can conditionally integrate Flow EVM Bridge for ERC-20s + +--- + +## Balance Query Architecture + +### Separated State Model + +The bridge maintains **independent state on each VM** rather than attempting real-time synchronization: + +#### EVM Side (FlowVaultsRequests) +```solidity +// Query escrowed funds awaiting processing +function getUserBalance(address user, address token) external view returns (uint256) { + return pendingUserBalances[user][token]; +} +``` + +**Use case**: Check how much FLOW a user has deposited but not yet processed into a Tide + +#### Cadence Side (FlowVaultsEVM / FlowVaults) +```cadence +// Query actual Tide positions and balances +access(all) fun getTideIDsForEVMAddress(_ evmAddress: String): [UInt64] + +// Users can then query individual Tide details through FlowVaults +access(all) fun getTideBalance(tideId: UInt64): UFix64 +``` + +**Use case**: Check actual Tide positions and their current balances including any yield earned + +### User Experience + +Users need to query **both sides** to get a complete picture: + +1. **Pending/Escrowed**: Query EVM contract for funds awaiting processing +2. **Active Positions**: Query Cadence for actual Tide balances + +**Frontend Integration**: +- Aggregate queries from both VMs in the UI +- Show combined view: "Pending: X FLOW | Active in Tides: Y FLOW" +- Use events to track state transitions + +**No Cross-VM Balance Sync**: The asynchronous nature of the bridge makes real-time balance synchronization impractical and unnecessary. Each VM is the source of truth for its domain. + +--- + +## Outstanding Questions & Alignment Needed + +### 1. **Multi-Token Support** +- **Question**: When do we integrate the Flow EVM Bridge for ERC-20 tokens? + - Phase 1: Native $FLOW only + - Phase 2: ERC-20 support via bridge +- **Question**: How do we handle token allow list? + - Which tokens from the Cadence side are supported? +- **Alignment**: "We can conditionally incorporate the EVM bridge with the already onboarded tokens on the Cadence side" + +### 2. **Request Lifecycle & Timeouts** +- **Question**: Can users cancel pending requests? + +### 3. **Balance Queries** +- **Clarification**: The system maintains separated state: + - EVM users query `FlowVaultsRequests.getUserBalance()` for escrowed funds awaiting processing + - For actual Tide balances, users must query Cadence directly (e.g., via read-only Cadence scripts) + - No real-time cross-VM balance synchronization +- **Question**: Should we provide a unified balance query interface that aggregates both? + - Potential solution: Off-chain indexer or frontend aggregation + +### 4. **State Consistency** +- **Question**: What happens if FlowVaultsEVM updates Cadence state but fails to update FlowVaultsRequests? + - Retry mechanism? + - Manual reconciliation? + +### 5. **Multi-Tide Management** +- **Question**: How do users specify which Tide to interact with for deposits/withdrawals? + - Request includes tideId parameter + - Automatic selection (e.g., newest Tide) +- **Question**: Limits on Tides per user? + +--- + +## Security Considerations + +### Access Control +1. **COA Authorization**: Only FlowVaultsEVM can control the COA +2. **Withdrawal Authorization**: Only COA can withdraw from FlowVaultsRequests +3. **Tide Ownership**: Tides are tagged by EVM address and non-transferable +4. **Request Validation**: Prevent duplicate processing of requests + +### Fund Safety +1. **Escrow Security**: Funds locked until successful processing +2. **Rollback Protection**: Failed operations don't lose funds + +### Unbounded Array Risk (Tide Storage) + +**Problem**: The current design stores all Tide IDs per user in an array (`tidesByEVMAddress: {String: [UInt64]}`). If a user creates many Tides, this array could grow unbounded, causing: +- **Iteration Issues**: Operations that verify Tide ownership must iterate through the array +- **Gas/Computation Limits**: Large arrays could exceed transaction limits +- **Locked Funds**: User funds could be stuck if array becomes too large to process + +--- + +## Implementation Phases + +### Phase 1: MVP (Native $FLOW only) +- Deploy FlowVaultsRequests contract to Flow EVM +- Deploy FlowVaultsEVM to Cadence +- Support CREATE_TIDE and CLOSE_TIDE operations +- Manual trigger for processRequests() + +### Phase 2: Full Operations +- Add DEPOSIT_TO_TIDE and WITHDRAW_FROM_TIDE +- Automated scheduled processing +- Event tracing and monitoring + +### Phase 3: Multi-Token Support +- Integrate Flow EVM Bridge for ERC-20 tokens +- Token allow list system +- Multi-token balance tracking + +### Phase 4: Optimization & Scale +- Request prioritization +- Batch processing optimization +- Advanced error handling and reconciliation + +--- + +## Testings + +### Integration Tests +- End-to-end CREATE_TIDE flow +- End-to-end WITHDRAW flow +- Multi-request batching +- Error scenarios and rollbacks + +### Stress Tests +- Maximum requests per transaction +- Computation limit analysis + +--- + +## Comparison with Existing FlowVaults Transactions + +The Cadence transactions provided (`create_tide.cdc`, `deposit_to_tide.cdc`, `withdraw_from_tide.cdc`, `close_tide.cdc`) demonstrate the native Cadence flow. Key differences in the EVM bridge approach: + +| Aspect | Native Cadence | EVM Bridge | +|--------|----------------|------------| +| User Identity | Flow account with BetaBadge | EVM address | +| Transaction Signer | User's Flow account | FlowVaultsEVM (on behalf of user) | +| Fund Source | User's Cadence vault | FlowVaultsRequests escrow | +| Tide Storage | User's TideManager | FlowVaultsEVM (tagged by EVM address) | +| Processing | Immediate (single txn) | Asynchronous (scheduled polling) | +| Beta Access | User holds BetaBadge | COA/Worker holds BetaBadge | + +--- + +## Next Steps + +1. **Alignment Meeting**: Review this document with Navid and Kan to resolve outstanding questions +2. **Technical Specification**: Detailed function signatures and state machine diagrams +3. **Prototype Development**: Implement Phase 1 MVP on testnet +4. **Security Audit**: Review design with security team before mainnet deployment +5. **Documentation**: User-facing guides for EVM users interacting with FlowVaults + +--- + +**Document Version**: 2.0 +**Last Updated**: November 3, 2025 +**Authors**: Lionel, Navid (based on discussions) +**Reviewed By**: Joshua (PR comments), Pending (Kan, engineering team) +**Updates**: Code extracts updated with final contract implementations; improvements based on Joshua's feedback diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f74f85 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# Flow Vaults EVM Integration + +Bridge Flow EVM users to Cadence-based yield farming through asynchronous cross-VM requests. + +## Quick Start +```bash +# 1. Start environment & deploy contracts +./local/setup_and_run_emulator.sh && ./local/deploy_full_stack.sh + +# 2. Create yield position from EVM +forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations --sig "runCreateTide()" --rpc-url localhost:8545 --broadcast --legacy + +# 3. Process request (Cadence worker) +flow transactions send ./cadence/transactions/process_requests.cdc --signer tidal --gas-limt 9999 +``` + +## Architecture + +**EVM Side:** Users deposit FLOW to `FlowVaultsRequests` contract and submit requests +**Cadence Side:** `FlowVaultsEVM` processes requests, creates/manages Tide positions +**Bridge:** COA (Cadence Owned Account) controls fund movement between VMs + +## Request Types & Operations + +All operations are performed using the unified `FlowVaultsTideOperations.s.sol` script: + +### CREATE_TIDE - Open new yield position +```bash +# With default amount (10 FLOW) +forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations --sig "runCreateTide()" --rpc-url localhost:8545 --broadcast --legacy + +# With custom amount +AMOUNT=100000000000000000000 forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations --sig "runCreateTide()" --rpc-url localhost:8545 --broadcast --legacy +``` + +### DEPOSIT_TO_TIDE - Add funds to existing position +```bash +# With default amount (10 FLOW) +forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations --sig "runDepositToTide(uint64)" 42 --rpc-url localhost:8545 --broadcast --legacy + +# With custom amount +AMOUNT=50000000000000000000 forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations --sig "runDepositToTide(uint64)" 42 --rpc-url localhost:8545 --broadcast --legacy +``` + +### WITHDRAW_FROM_TIDE - Withdraw earnings +```bash +forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations --sig "runWithdrawFromTide(uint64,uint256)" 42 30000000000000000000 --rpc-url localhost:8545 --broadcast --legacy +``` + +### CLOSE_TIDE - Close position and return all funds +```bash +forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations --sig "runCloseTide(uint64)" 42 --rpc-url localhost:8545 --broadcast --legacy +``` + +See `solidity/script/TIDE_OPERATIONS.md` for detailed usage documentation. + +## Key Addresses + +| Component | Address | +|-----------|---------| +| RPC | `localhost:8545` | +| FlowVaultsRequests | `0x153b84F377C6C7a7D93Bd9a717E48097Ca6Cfd11` | +| Deployer | `0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF` | +| User A | `0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69` | + +## How It Works +``` +EVM User → FlowVaultsRequests (escrow FLOW) → Worker polls requests → +COA bridges funds → Create Tide on Cadence → Update EVM state +``` + +## Status + +✅ **CREATE_TIDE** - Fully working +🚧 **DEPOSIT/WITHDRAW/CLOSE** - In development + +--- + +**Built on Flow** | [Docs](./docs) | [Architecture](./DESIGN.md) \ No newline at end of file diff --git a/cadence/contracts/FlowVaultsEVM.cdc b/cadence/contracts/FlowVaultsEVM.cdc new file mode 100644 index 0000000..6716b10 --- /dev/null +++ b/cadence/contracts/FlowVaultsEVM.cdc @@ -0,0 +1,803 @@ +import "FungibleToken" +import "FlowToken" +import "EVM" + +import "FlowVaults" +import "FlowVaultsClosedBeta" + +/// FlowVaultsEVM: Bridge contract that processes requests from EVM users +/// and manages their Tide positions in Cadence +access(all) contract FlowVaultsEVM { + + // ======================================== + // Enums + // ======================================== + + /// Request Type (matching Solidity enum RequestType) + access(all) enum RequestType: UInt8 { + access(all) case CREATE_TIDE // rawValue = 0 + access(all) case DEPOSIT_TO_TIDE // rawValue = 1 + access(all) case WITHDRAW_FROM_TIDE // rawValue = 2 + access(all) case CLOSE_TIDE // rawValue = 3 + } + + /// Request Status (matching Solidity enum RequestStatus) + access(all) enum RequestStatus: UInt8 { + access(all) case PENDING // rawValue = 0 + access(all) case COMPLETED // rawValue = 1 + access(all) case FAILED // rawValue = 2 + } + + // ======================================== + // Constants + // ======================================== + + /// Maximum requests to process per transaction + /// Updatable by Admin for performance tuning + access(all) var MAX_REQUESTS_PER_TX: Int + + // ======================================== + // Paths + // ======================================== + + access(all) let WorkerStoragePath: StoragePath + access(all) let WorkerPublicPath: PublicPath + access(all) let AdminStoragePath: StoragePath + + // ======================================== + // State + // ======================================== + + access(all) let tidesByEVMAddress: {String: [UInt64]} + access(all) var flowVaultsRequestsAddress: EVM.EVMAddress? + + // ======================================== + // Events + // ======================================== + + access(all) event WorkerInitialized(coaAddress: String) + access(all) event FlowVaultsRequestsAddressSet(address: String) + access(all) event RequestsProcessed(count: Int, successful: Int, failed: Int) + access(all) event TideCreatedForEVMUser(evmAddress: String, tideId: UInt64, amount: UFix64) + access(all) event TideDepositedForEVMUser(evmAddress: String, tideId: UInt64, amount: UFix64) + access(all) event TideWithdrawnForEVMUser(evmAddress: String, tideId: UInt64, amount: UFix64) + access(all) event TideClosedForEVMUser(evmAddress: String, tideId: UInt64, amountReturned: UFix64) + access(all) event RequestFailed(requestId: UInt256, reason: String) + access(all) event MaxRequestsPerTxUpdated(oldValue: Int, newValue: Int) + + // ======================================== + // Structs + // ======================================== + + access(all) struct EVMRequest { + access(all) let id: UInt256 + access(all) let user: EVM.EVMAddress + access(all) let requestType: UInt8 + access(all) let status: UInt8 + access(all) let tokenAddress: EVM.EVMAddress + access(all) let amount: UInt256 + access(all) let tideId: UInt64 + access(all) let timestamp: UInt256 + access(all) let message: String + access(all) let vaultIdentifier: String + access(all) let strategyIdentifier: String + + init( + id: UInt256, + user: EVM.EVMAddress, + requestType: UInt8, + status: UInt8, + tokenAddress: EVM.EVMAddress, + amount: UInt256, + tideId: UInt64, + timestamp: UInt256, + message: String, + vaultIdentifier: String, + strategyIdentifier: String + ) { + self.id = id + self.user = user + self.requestType = requestType + self.status = status + self.tokenAddress = tokenAddress + self.amount = amount + self.tideId = tideId + self.timestamp = timestamp + self.message = message + self.vaultIdentifier = vaultIdentifier + self.strategyIdentifier = strategyIdentifier + } + } + + access(all) struct ProcessResult { + access(all) let success: Bool + access(all) let tideId: UInt64 + access(all) let message: String + + init(success: Bool, tideId: UInt64, message: String) { + self.success = success + self.tideId = tideId + self.message = message + } + } + + // ======================================== + // Admin Resource + // ======================================== + + access(all) resource Admin { + access(all) fun setFlowVaultsRequestsAddress(_ address: EVM.EVMAddress) { + pre { + FlowVaultsEVM.flowVaultsRequestsAddress == nil: "FlowVaultsRequests address already set" + } + FlowVaultsEVM.flowVaultsRequestsAddress = address + emit FlowVaultsRequestsAddressSet(address: address.toString()) + } + + access(all) fun updateFlowVaultsRequestsAddress(_ address: EVM.EVMAddress) { + FlowVaultsEVM.flowVaultsRequestsAddress = address + emit FlowVaultsRequestsAddressSet(address: address.toString()) + } + + /// Update the maximum number of requests to process per transaction + /// NEW: Allows runtime tuning without redeployment + access(all) fun updateMaxRequestsPerTx(_ newMax: Int) { + pre { + newMax > 0: "MAX_REQUESTS_PER_TX must be greater than 0" + newMax <= 100: "MAX_REQUESTS_PER_TX should not exceed 100 for gas safety" + } + + let oldMax = FlowVaultsEVM.MAX_REQUESTS_PER_TX + FlowVaultsEVM.MAX_REQUESTS_PER_TX = newMax + + emit MaxRequestsPerTxUpdated(oldValue: oldMax, newValue: newMax) + } + + access(all) fun createWorker( + coaCap: Capability, + betaBadgeCap: Capability + ): @Worker { + let worker <- create Worker( + coaCap: coaCap, + betaBadgeCap: betaBadgeCap + ) + emit WorkerInitialized(coaAddress: worker.getCOAAddressString()) + return <-worker + } + } + + // ======================================== + // Worker Resource + // ======================================== + + access(all) resource Worker { + access(self) let coaCap: Capability + access(self) let tideManager: @FlowVaults.TideManager + access(self) let betaBadgeCap: Capability + + init( + coaCap: Capability, + betaBadgeCap: Capability + ) { + pre { + coaCap.check(): "COA capability is invalid" + } + + self.coaCap = coaCap + self.betaBadgeCap = betaBadgeCap + + let betaBadge = betaBadgeCap.borrow() + ?? panic("Could not borrow beta badge capability") + + self.tideManager <- FlowVaults.createTideManager(betaRef: betaBadge) + } + + /// Get reference to COA + access(self) fun getCOARef(): auth(EVM.Call, EVM.Withdraw) &EVM.CadenceOwnedAccount { + return self.coaCap.borrow() + ?? panic("Could not borrow COA capability") + } + + access(self) fun getBetaReference(): auth(FlowVaultsClosedBeta.Beta) &FlowVaultsClosedBeta.BetaBadge { + return self.betaBadgeCap.borrow() + ?? panic("Could not borrow beta badge capability") + } + + access(all) fun getCOAAddressString(): String { + return self.getCOARef().address().toString() + } + + /// Process pending requests (up to MAX_REQUESTS_PER_TX) + /// External handler manages scheduling + access(all) fun processRequests() { + pre { + FlowVaultsEVM.flowVaultsRequestsAddress != nil: "FlowVaultsRequests address not set" + } + + // 1. Get count of pending requests (lightweight) + let pendingIds = self.getPendingRequestIdsFromEVM() + let totalPending = pendingIds.length + + log("Total pending requests: ".concat(totalPending.toString())) + + // 2. Fetch only the batch we'll process (up to MAX_REQUESTS_PER_TX) + let requestsToProcess = self.getPendingRequestsFromEVM() + let batchSize = requestsToProcess.length + + log("Batch size to process: ".concat(batchSize.toString())) + + if batchSize == 0 { + emit RequestsProcessed(count: 0, successful: 0, failed: 0) + return + } + + var successCount = 0 + var failCount = 0 + var i = 0 + + while i < batchSize { + let request = requestsToProcess[i] + + log("Processing request: ".concat(request.id.toString())) + log("Request type: ".concat(request.requestType.toString())) + log("User: ".concat(request.user.toString())) + log("Amount: ".concat(request.amount.toString())) + + let success = self.processRequestSafely(request) + if success { + successCount = successCount + 1 + } else { + failCount = failCount + 1 + } + i = i + 1 + } + + emit RequestsProcessed(count: batchSize, successful: successCount, failed: failCount) + } + + access(self) fun processRequestSafely(_ request: EVMRequest): Bool { + var success = false + var tideId: UInt64 = 0 + var message = "" + + switch request.requestType { + case FlowVaultsEVM.RequestType.CREATE_TIDE.rawValue: + let result = self.processCreateTide(request) + success = result.success + tideId = result.tideId + message = result.message + case FlowVaultsEVM.RequestType.DEPOSIT_TO_TIDE.rawValue: + let result = self.processDepositToTide(request) + success = result.success + tideId = request.tideId + message = result.message + case FlowVaultsEVM.RequestType.WITHDRAW_FROM_TIDE.rawValue: + let result = self.processWithdrawFromTide(request) + success = result.success + tideId = request.tideId + message = result.message + case FlowVaultsEVM.RequestType.CLOSE_TIDE.rawValue: + let result = self.processCloseTideWithMessage(request) + success = result.success + tideId = request.tideId + message = result.message + default: + success = false + message = "Unknown request type: ".concat(request.requestType.toString()).concat(" for request ID ").concat(request.id.toString()) + } + + let finalStatus = success + ? FlowVaultsEVM.RequestStatus.COMPLETED.rawValue + : FlowVaultsEVM.RequestStatus.FAILED.rawValue + + self.updateRequestStatus( + requestId: request.id, + status: finalStatus, + tideId: tideId, + message: message + ) + + if !success { + emit RequestFailed(requestId: request.id, reason: message) + } + + return success + } + + access(self) fun processCreateTide(_ request: EVMRequest): ProcessResult { + let vaultIdentifier = request.vaultIdentifier + let strategyIdentifier = request.strategyIdentifier + + let amount = FlowVaultsEVM.ufix64FromUInt256(request.amount) + log("Creating Tide for amount: ".concat(amount.toString())) + + let vault <- self.withdrawFundsFromEVM(amount: amount) + + let vaultType = vault.getType() + if vaultType.identifier != vaultIdentifier { + destroy vault + return ProcessResult( + success: false, + tideId: 0, + message: "Vault type mismatch: expected ".concat(vaultIdentifier).concat(", got ").concat(vaultType.identifier) + ) + } + + let strategyType = CompositeType(strategyIdentifier) + if strategyType == nil { + destroy vault + return ProcessResult( + success: false, + tideId: 0, + message: "Invalid strategyIdentifier: ".concat(strategyIdentifier) + ) + } + + let betaRef = self.getBetaReference() + let tidesBeforeCreate = self.tideManager.getIDs() + + self.tideManager.createTide( + betaRef: betaRef, + strategyType: strategyType!, + withVault: <-vault + ) + + let tidesAfterCreate = self.tideManager.getIDs() + var tideId = UInt64.max + for id in tidesAfterCreate { + if !tidesBeforeCreate.contains(id) { + tideId = id + break + } + } + + if tideId == UInt64.max { + return ProcessResult( + success: false, + tideId: 0, + message: "Failed to find newly created Tide ID after creation for request ".concat(request.id.toString()) + ) + } + + let evmAddr = request.user.toString() + if FlowVaultsEVM.tidesByEVMAddress[evmAddr] == nil { + FlowVaultsEVM.tidesByEVMAddress[evmAddr] = [] + } + FlowVaultsEVM.tidesByEVMAddress[evmAddr]!.append(tideId) + + let currentBalance = self.getUserBalanceFromEVM(user: request.user, tokenAddress: request.tokenAddress) + let amountUInt256 = FlowVaultsEVM.uint256FromUFix64(amount) + let newBalance = currentBalance >= amountUInt256 + ? currentBalance - amountUInt256 + : 0 as UInt256 + + self.updateUserBalance( + user: request.user, + tokenAddress: request.tokenAddress, + newBalance: newBalance + ) + + emit TideCreatedForEVMUser(evmAddress: evmAddr, tideId: tideId, amount: amount) + + return ProcessResult( + success: true, + tideId: tideId, + message: "Tide ID ".concat(tideId.toString()).concat(" created successfully with amount ").concat(amount.toString()).concat(" FLOW") + ) + } + + access(self) fun processCloseTideWithMessage(_ request: EVMRequest): ProcessResult { + let evmAddr = request.user.toString() + + if let userTides = FlowVaultsEVM.tidesByEVMAddress[evmAddr] { + if !userTides.contains(request.tideId) { + return ProcessResult( + success: false, + tideId: request.tideId, + message: "User ".concat(evmAddr).concat(" does not own Tide ID ").concat(request.tideId.toString()) + ) + } + } else { + return ProcessResult( + success: false, + tideId: request.tideId, + message: "User ".concat(evmAddr).concat(" has no Tides registered") + ) + } + + let vault <- self.tideManager.closeTide(request.tideId) + let amount = vault.balance + + self.bridgeFundsToEVMUser(vault: <-vault, recipient: request.user) + + if let index = FlowVaultsEVM.tidesByEVMAddress[evmAddr]!.firstIndex(of: request.tideId) { + let _ = FlowVaultsEVM.tidesByEVMAddress[evmAddr]!.remove(at: index) + } + + emit TideClosedForEVMUser(evmAddress: evmAddr, tideId: request.tideId, amountReturned: amount) + + return ProcessResult( + success: true, + tideId: request.tideId, + message: "Tide ID ".concat(request.tideId.toString()).concat(" closed successfully, returned ").concat(amount.toString()).concat(" FLOW") + ) + } + + access(self) fun processDepositToTide(_ request: EVMRequest): ProcessResult { + let evmAddr = request.user.toString() + + // 1. Verify user owns the Tide + if let userTides = FlowVaultsEVM.tidesByEVMAddress[evmAddr] { + if !userTides.contains(request.tideId) { + return ProcessResult( + success: false, + tideId: request.tideId, + message: "User ".concat(evmAddr).concat(" does not own Tide ID ").concat(request.tideId.toString()) + ) + } + } else { + return ProcessResult( + success: false, + tideId: request.tideId, + message: "User ".concat(evmAddr).concat(" has no Tides registered") + ) + } + + // 2. Withdraw funds from EVM + let amount = FlowVaultsEVM.ufix64FromUInt256(request.amount) + log("Depositing to Tide for amount: ".concat(amount.toString())) + + let vault <- self.withdrawFundsFromEVM(amount: amount) + + // 3. Deposit to existing Tide + let betaRef = self.getBetaReference() + self.tideManager.depositToTide(betaRef: betaRef, request.tideId, from: <-vault) + + // 4. Subtract amount from current balance instead of setting to 0 + let currentBalance = self.getUserBalanceFromEVM(user: request.user, tokenAddress: request.tokenAddress) + let newBalance = currentBalance >= request.amount + ? currentBalance - request.amount + : 0 as UInt256 + + self.updateUserBalance( + user: request.user, + tokenAddress: request.tokenAddress, + newBalance: newBalance + ) + + emit TideDepositedForEVMUser(evmAddress: evmAddr, tideId: request.tideId, amount: amount) + + return ProcessResult( + success: true, + tideId: request.tideId, + message: "Deposited ".concat(amount.toString()).concat(" FLOW to Tide ID ").concat(request.tideId.toString()) + ) + } + + access(self) fun processWithdrawFromTide(_ request: EVMRequest): ProcessResult { + let evmAddr = request.user.toString() + + // 1. Verify user owns the Tide + if let userTides = FlowVaultsEVM.tidesByEVMAddress[evmAddr] { + if !userTides.contains(request.tideId) { + return ProcessResult( + success: false, + tideId: request.tideId, + message: "User ".concat(evmAddr).concat(" does not own Tide ID ").concat(request.tideId.toString()) + ) + } + } else { + return ProcessResult( + success: false, + tideId: request.tideId, + message: "User ".concat(evmAddr).concat(" has no Tides registered") + ) + } + + // 2. Withdraw from Tide + let amount = FlowVaultsEVM.ufix64FromUInt256(request.amount) + log("Withdrawing from Tide for amount: ".concat(amount.toString())) + + let vault <- self.tideManager.withdrawFromTide(request.tideId, amount: amount) + + // 3. Bridge funds back to EVM user + let actualAmount = vault.balance + self.bridgeFundsToEVMUser(vault: <-vault, recipient: request.user) + + emit TideWithdrawnForEVMUser(evmAddress: evmAddr, tideId: request.tideId, amount: actualAmount) + + return ProcessResult( + success: true, + tideId: request.tideId, + message: "Withdrew ".concat(actualAmount.toString()).concat(" FLOW from Tide ID ").concat(request.tideId.toString()) + ) + } + + access(self) fun withdrawFundsFromEVM(amount: UFix64): @{FungibleToken.Vault} { + let amountUInt256 = FlowVaultsEVM.uint256FromUFix64(amount) + let nativeFlowAddress = EVM.addressFromString("0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF") + + let calldata = EVM.encodeABIWithSignature( + "withdrawFunds(address,uint256)", + [nativeFlowAddress, amountUInt256] + ) + + let result = self.getCOARef().call( + to: FlowVaultsEVM.flowVaultsRequestsAddress!, + data: calldata, + gasLimit: 100_000, + value: EVM.Balance(attoflow: 0) + ) + + // If EVM call fails, decode error and panic + // This causes the entire transaction to revert + if result.status != EVM.Status.successful { + let errorMsg = FlowVaultsEVM.decodeEVMError(result.data) + panic("withdrawFunds call failed: ".concat(errorMsg)) + } + + // Convert UFix64 amount to attoflow (10^18) for EVM.Balance + let balance = FlowVaultsEVM.balanceFromUFix64(amount) + let vault <- self.getCOARef().withdraw(balance: balance) + + return <-vault + } + + access(self) fun bridgeFundsToEVMUser(vault: @{FungibleToken.Vault}, recipient: EVM.EVMAddress) { + let amount = vault.balance + + // Deposit vault to COA first + self.getCOARef().deposit(from: <-vault as! @FlowToken.Vault) + + // Convert UFix64 amount to attoflow (10^18) and withdraw as EVM balance + let balance = FlowVaultsEVM.balanceFromUFix64(amount) + recipient.deposit(from: <-self.getCOARef().withdraw(balance: balance)) + } + + access(self) fun updateRequestStatus(requestId: UInt256, status: UInt8, tideId: UInt64, message: String) { + let calldata = EVM.encodeABIWithSignature( + "updateRequestStatus(uint256,uint8,uint64,string)", + [requestId, status, tideId, message] + ) + + let result = self.getCOARef().call( + to: FlowVaultsEVM.flowVaultsRequestsAddress!, + data: calldata, + gasLimit: 1_000_000, + value: EVM.Balance(attoflow: 0) + ) + + // If EVM call fails, decode error and panic + // This causes the entire transaction to revert + if result.status != EVM.Status.successful { + let errorMsg = FlowVaultsEVM.decodeEVMError(result.data) + panic("updateRequestStatus call failed: ".concat(errorMsg)) + } + } + + access(self) fun updateUserBalance(user: EVM.EVMAddress, tokenAddress: EVM.EVMAddress, newBalance: UInt256) { + let calldata = EVM.encodeABIWithSignature( + "updateUserBalance(address,address,uint256)", + [user, tokenAddress, newBalance] + ) + + let result = self.getCOARef().call( + to: FlowVaultsEVM.flowVaultsRequestsAddress!, + data: calldata, + gasLimit: 100_000, + value: EVM.Balance(attoflow: 0) + ) + + // If EVM call fails, decode error and panic + // This causes the entire transaction to revert + if result.status != EVM.Status.successful { + let errorMsg = FlowVaultsEVM.decodeEVMError(result.data) + panic("updateUserBalance call failed: ".concat(errorMsg)) + } + } + + /// Get user's current balance from EVM contract + access(self) fun getUserBalanceFromEVM(user: EVM.EVMAddress, tokenAddress: EVM.EVMAddress): UInt256 { + let calldata = EVM.encodeABIWithSignature( + "getUserBalance(address,address)", + [user, tokenAddress] + ) + + let callResult = self.getCOARef().dryCall( + to: FlowVaultsEVM.flowVaultsRequestsAddress!, + data: calldata, + gasLimit: 100_000, + value: EVM.Balance(attoflow: 0) + ) + + if callResult.status != EVM.Status.successful { + let errorMsg = FlowVaultsEVM.decodeEVMError(callResult.data) + panic("getUserBalance call failed: ".concat(errorMsg)) + } + + let decoded = EVM.decodeABI( + types: [Type()], + data: callResult.data + ) + + return decoded[0] as! UInt256 + } + + /// Get pending request IDs from FlowVaultsRequests contract (lightweight) + /// Used for counting total pending requests without fetching full data + access(all) fun getPendingRequestIdsFromEVM(): [UInt256] { + let calldata = EVM.encodeABIWithSignature("getPendingRequestIds()", []) + + let callResult = self.getCOARef().dryCall( + to: FlowVaultsEVM.flowVaultsRequestsAddress!, + data: calldata, + gasLimit: 500_000, + value: EVM.Balance(attoflow: 0) + ) + + // If EVM call fails, decode error and panic + if callResult.status != EVM.Status.successful { + let errorMsg = FlowVaultsEVM.decodeEVMError(callResult.data) + panic("getPendingRequestIds call failed: ".concat(errorMsg)) + } + + let decoded = EVM.decodeABI( + types: [Type<[UInt256]>()], + data: callResult.data + ) + + return decoded[0] as! [UInt256] + } + + /// Get pending requests from FlowVaultsRequests contract + /// Now fetches only up to MAX_REQUESTS_PER_TX for efficiency + access(all) fun getPendingRequestsFromEVM(): [EVMRequest] { + // Call with limit parameter to only fetch what we'll process + let limit = UInt256(FlowVaultsEVM.MAX_REQUESTS_PER_TX) + let calldata = EVM.encodeABIWithSignature("getPendingRequestsUnpacked(uint256)", [limit]) + + let callResult = self.getCOARef().dryCall( + to: FlowVaultsEVM.flowVaultsRequestsAddress!, + data: calldata, + gasLimit: 15_000_000, + value: EVM.Balance(attoflow: 0) + ) + + log("=== EVM Call Result ===") + log("Status: ".concat(callResult.status == EVM.Status.successful ? "SUCCESSFUL" : "FAILED")) + log("Requested limit: ".concat(limit.toString())) + log("Gas Used: ".concat(callResult.gasUsed.toString())) + log("Data Length: ".concat(callResult.data.length.toString())) + + // If EVM call fails, decode error and panic + if callResult.status != EVM.Status.successful { + let errorMsg = FlowVaultsEVM.decodeEVMError(callResult.data) + panic("getPendingRequestsUnpacked call failed: ".concat(errorMsg)) + } + + let decoded = EVM.decodeABI( + types: [ + Type<[UInt256]>(), + Type<[EVM.EVMAddress]>(), + Type<[UInt8]>(), + Type<[UInt8]>(), + Type<[EVM.EVMAddress]>(), + Type<[UInt256]>(), + Type<[UInt64]>(), + Type<[UInt256]>(), + Type<[String]>(), + Type<[String]>(), + Type<[String]>() + ], + data: callResult.data + ) + + let ids = decoded[0] as! [UInt256] + let users = decoded[1] as! [EVM.EVMAddress] + let requestTypes = decoded[2] as! [UInt8] + let statuses = decoded[3] as! [UInt8] + let tokenAddresses = decoded[4] as! [EVM.EVMAddress] + let amounts = decoded[5] as! [UInt256] + let tideIds = decoded[6] as! [UInt64] + let timestamps = decoded[7] as! [UInt256] + let messages = decoded[8] as! [String] + let vaultIdentifiers = decoded[9] as! [String] + let strategyIdentifiers = decoded[10] as! [String] + + let requests: [EVMRequest] = [] + var i = 0 + while i < ids.length { + let request = EVMRequest( + id: ids[i], + user: users[i], + requestType: requestTypes[i], + status: statuses[i], + tokenAddress: tokenAddresses[i], + amount: amounts[i], + tideId: tideIds[i], + timestamp: timestamps[i], + message: messages[i], + vaultIdentifier: vaultIdentifiers[i], + strategyIdentifier: strategyIdentifiers[i] + ) + requests.append(request) + i = i + 1 + } + + return requests + } + } + + // ======================================== + // Public Functions + // ======================================== + + access(all) fun getTideIDsForEVMAddress(_ evmAddress: String): [UInt64] { + return self.tidesByEVMAddress[evmAddress] ?? [] + } + + access(all) fun getFlowVaultsRequestsAddress(): EVM.EVMAddress? { + return self.flowVaultsRequestsAddress + } + + access(self) fun ufix64FromUInt256(_ value: UInt256): UFix64 { + let scaled = value / 10_000_000_000 + return UFix64(scaled) / 100_000_000.0 + } + + access(self) fun uint256FromUFix64(_ value: UFix64): UInt256 { + let rawValue = UInt64(value * 100_000_000.0) + return UInt256(rawValue) * 10_000_000_000 + } + + /// Convert UFix64 (8 decimals) to EVM.Balance (attoflow, 18 decimals) + /// UFix64: 1.0 = 1 FLOW with 8 decimal places + /// Attoflow: 1 FLOW = 10^18 attoflow + access(self) fun balanceFromUFix64(_ value: UFix64): EVM.Balance { + // Convert UFix64 to its base unit representation (8 decimals) + let flowUnits = UInt64(value * 100_000_000.0) + // Scale from 8 decimals to 18 decimals (attoflow) + let attoflowAmount = UInt(flowUnits) * 10_000_000_000 + return EVM.Balance(attoflow: attoflowAmount) + } + + /// Decode error message from EVM revert data + /// EVM reverts typically encode as: Error(string) selector (0x08c379a0) + ABI-encoded string + access(self) fun decodeEVMError(_ data: [UInt8]): String { + // Check if data starts with Error(string) selector: 0x08c379a0 + if data.length >= 4 { + let selector = (UInt32(data[0]) << 24) | (UInt32(data[1]) << 16) | (UInt32(data[2]) << 8) | UInt32(data[3]) + if selector == 0x08c379a0 && data.length > 4 { + // Try to decode the ABI-encoded string + let payload = data.slice(from: 4, upTo: data.length) + let decoded = EVM.decodeABI(types: [Type()], data: payload) + if decoded.length > 0 { + if let errorMsg = decoded[0] as? String { + return errorMsg + } + } + } + } + // Fallback: return hex representation of revert data + return "EVM revert data: 0x".concat(String.encodeHex(data)) + } + + // ======================================== + // Initialization + // ======================================== + + init() { + self.WorkerStoragePath = /storage/flowVaultsEVM + self.WorkerPublicPath = /public/flowVaultsEVM + self.AdminStoragePath = /storage/flowVaultsEVMAdmin + + // Initialize with conservative batch size + self.MAX_REQUESTS_PER_TX = 1 + + self.tidesByEVMAddress = {} + self.flowVaultsRequestsAddress = nil + + let admin <- create Admin() + self.account.storage.save(<-admin, to: self.AdminStoragePath) + } +} diff --git a/cadence/contracts/FlowVaultsTransactionHandler.cdc b/cadence/contracts/FlowVaultsTransactionHandler.cdc new file mode 100644 index 0000000..cc67abe --- /dev/null +++ b/cadence/contracts/FlowVaultsTransactionHandler.cdc @@ -0,0 +1,286 @@ +import "FlowTransactionScheduler" +import "FlowTransactionSchedulerUtils" +import "FlowVaultsEVM" +import "FlowToken" +import "FungibleToken" + +/// Handler contract for scheduled FlowVaultsEVM request processing +/// WITH AUTO-SCHEDULING: After each execution, automatically schedules the next one +access(all) contract FlowVaultsTransactionHandler { + + // ======================================== + // Constants + // ======================================== + + access(all) let HandlerStoragePath: StoragePath + access(all) let HandlerPublicPath: PublicPath + access(all) let AdminStoragePath: StoragePath + + /// 5 delay levels (in seconds) + access(all) let DELAY_LEVELS: [UFix64] + + /// Thresholds for 5 delay levels (pending request counts) + access(all) let LOAD_THRESHOLDS: [Int] + + // ======================================== + // State + // ======================================== + + /// When true, scheduled executions will skip processing and not schedule the next execution + access(all) var isPaused: Bool + + // ======================================== + // Events + // ======================================== + + access(all) event HandlerPaused() + access(all) event HandlerUnpaused() + + access(all) event ScheduledExecutionTriggered( + transactionId: UInt64, + pendingRequests: Int, + delayLevel: Int, + nextExecutionDelay: UFix64 + ) + + access(all) event NextExecutionScheduled( + transactionId: UInt64, + scheduledFor: UFix64, + delaySeconds: UFix64, + pendingRequests: Int + ) + + // ======================================== + // Admin Resource + // ======================================== + + access(all) resource Admin { + access(all) fun pause() { + FlowVaultsTransactionHandler.isPaused = true + emit HandlerPaused() + } + + access(all) fun unpause() { + FlowVaultsTransactionHandler.isPaused = false + emit HandlerUnpaused() + } + } + + // ======================================== + // Handler Resource + // ======================================== + + access(all) resource Handler: FlowTransactionScheduler.TransactionHandler { + + access(self) let workerCap: Capability<&FlowVaultsEVM.Worker> + access(self) var executionCount: UInt64 + access(self) var lastExecutionTime: UFix64? + + init(workerCap: Capability<&FlowVaultsEVM.Worker>) { + self.workerCap = workerCap + self.executionCount = 0 + self.lastExecutionTime = nil + } + + access(FlowTransactionScheduler.Execute) fun executeTransaction(id: UInt64, data: AnyStruct?) { + log("=== FlowVaultsEVM Scheduled Execution Started ===") + log("Transaction ID: ".concat(id.toString())) + + // Check if paused + if FlowVaultsTransactionHandler.isPaused { + log("⏸️ Handler is PAUSED - skipping execution and NOT scheduling next") + log("=== FlowVaultsEVM Scheduled Execution Skipped (Paused) ===") + return + } + + let worker = self.workerCap.borrow() + ?? panic("Could not borrow Worker capability") + + let pendingRequestsBefore = self.getPendingRequestCount(worker) + log("Pending Requests Before: ".concat(pendingRequestsBefore.toString())) + + worker.processRequests() + + let pendingRequestsAfter = self.getPendingRequestCount(worker) + log("Pending Requests After: ".concat(pendingRequestsAfter.toString())) + + self.executionCount = self.executionCount + 1 + self.lastExecutionTime = getCurrentBlock().timestamp + + let delayLevel = FlowVaultsTransactionHandler.getDelayLevel(pendingRequestsAfter) + let nextDelay = FlowVaultsTransactionHandler.DELAY_LEVELS[delayLevel] + + emit ScheduledExecutionTriggered( + transactionId: id, + pendingRequests: pendingRequestsAfter, + delayLevel: delayLevel, + nextExecutionDelay: nextDelay + ) + + // AUTO-SCHEDULE: Schedule the next execution based on remaining workload + self.scheduleNextExecution(nextDelay: nextDelay, pendingRequests: pendingRequestsAfter) + + log("=== FlowVaultsEVM Scheduled Execution Complete ===") + } + + /// Schedule the next execution automatically + access(self) fun scheduleNextExecution(nextDelay: UFix64, pendingRequests: Int) { + log("=== Auto-Scheduling Next Execution ===") + + let future = getCurrentBlock().timestamp + nextDelay + let priority = FlowTransactionScheduler.Priority.Medium + let executionEffort: UInt64 = 7499 + + // Estimate fees for the next execution + let estimate = FlowTransactionScheduler.estimate( + data: nil, + timestamp: future, + priority: priority, + executionEffort: executionEffort + ) + + // Validate the estimate + assert( + estimate.timestamp != nil || priority == FlowTransactionScheduler.Priority.Low, + message: estimate.error ?? "estimation failed" + ) + + // Withdraw fees from contract account + let vaultRef = FlowVaultsTransactionHandler.account.storage + .borrow(from: /storage/flowTokenVault) + ?? panic("missing FlowToken vault on contract account") + + let fees <- vaultRef.withdraw(amount: estimate.flowFee ?? 0.0) as! @FlowToken.Vault + + // Get the manager from contract account storage + let manager = FlowVaultsTransactionHandler.account.storage + .borrow( + from: FlowTransactionSchedulerUtils.managerStoragePath + ) + ?? panic("Could not borrow Manager reference from contract account") + + // Get the handler type identifier - use the first (and should be only) handler type + let handlerTypeIdentifiers = manager.getHandlerTypeIdentifiers() + assert(handlerTypeIdentifiers.keys.length > 0, message: "No handler types found in manager") + let handlerTypeIdentifier = handlerTypeIdentifiers.keys[0] + + // Schedule using the existing handler + let transactionId = manager.scheduleByHandler( + handlerTypeIdentifier: handlerTypeIdentifier, + handlerUUID: nil, + data: nil, + timestamp: future, + priority: priority, + executionEffort: executionEffort, + fees: <-fees + ) + + emit NextExecutionScheduled( + transactionId: transactionId, + scheduledFor: future, + delaySeconds: nextDelay, + pendingRequests: pendingRequests + ) + + log("Next execution scheduled for: ".concat(future.toString())) + log("Transaction ID: ".concat(transactionId.toString())) + log("Delay: ".concat(nextDelay.toString()).concat(" seconds")) + log("=== Auto-Scheduling Complete ===") + } + + access(all) view fun getViews(): [Type] { + return [Type(), Type()] + } + + access(all) fun resolveView(_ view: Type): AnyStruct? { + switch view { + case Type(): + return FlowVaultsTransactionHandler.HandlerStoragePath + case Type(): + return FlowVaultsTransactionHandler.HandlerPublicPath + default: + return nil + } + } + + access(self) fun getPendingRequestCount(_ worker: &FlowVaultsEVM.Worker): Int { + let requests = worker.getPendingRequestIdsFromEVM() + return requests.length + } + + access(all) fun getStats(): {String: AnyStruct} { + return { + "executionCount": self.executionCount, + "lastExecutionTime": self.lastExecutionTime + } + } + } + + // ======================================== + // Public Functions + // ======================================== + + access(all) fun createHandler(workerCap: Capability<&FlowVaultsEVM.Worker>): @Handler { + return <- create Handler(workerCap: workerCap) + } + + /// Determine delay level based on pending request count (5 levels) + access(all) fun getDelayLevel(_ pendingCount: Int): Int { + var level = 4 // Default to slowest + + var i = 0 + while i < FlowVaultsTransactionHandler.LOAD_THRESHOLDS.length { + if pendingCount >= FlowVaultsTransactionHandler.LOAD_THRESHOLDS[i] { + level = i + break + } + i = i + 1 + } + + return level + } + + access(all) fun getDelayForPendingCount(_ pendingCount: Int): UFix64 { + let level = self.getDelayLevel(pendingCount) + return self.DELAY_LEVELS[level] + } + + access(all) fun isPausedState(): Bool { + return self.isPaused + } + + // ======================================== + // Initialization + // ======================================== + + init() { + self.HandlerStoragePath = /storage/FlowVaultsTransactionHandler + self.HandlerPublicPath = /public/FlowVaultsTransactionHandler + self.AdminStoragePath = /storage/FlowVaultsTransactionHandlerAdmin + + // Initialize as unpaused + self.isPaused = false + + // 5 delay levels (simplified) + self.DELAY_LEVELS = [ + 5.0, // Level 0: High load (>=50 requests) - 5s + 15.0, // Level 1: Medium-high load (>=20 requests) - 15s + 30.0, // Level 2: Medium load (>=10 requests) - 30s + 45.0, // Level 3: Low load (>=5 requests) - 45s + 60.0 // Level 4: Very low/Idle (<5 requests) - 60s + ] + + // 5 thresholds + self.LOAD_THRESHOLDS = [ + 50, // Level 0: High load + 20, // Level 1: Medium-high load + 10, // Level 2: Medium load + 5, // Level 3: Low load + 0 // Level 4: Very low/Idle + ] + + // Create and save Admin resource + let admin <- create Admin() + self.account.storage.save(<-admin, to: self.AdminStoragePath) + } +} diff --git a/cadence/scripts/check_coa.cdc b/cadence/scripts/check_coa.cdc new file mode 100644 index 0000000..919bb54 --- /dev/null +++ b/cadence/scripts/check_coa.cdc @@ -0,0 +1,15 @@ +// check_coa.cdc +import "EVM" + +access(all) fun main(address: Address): String { + let account = getAccount(address) + + // Check if COA exists at standard path + let coaType = account.storage.type(at: /storage/evm) + + if coaType == nil { + return "❌ No COA found at /storage/evm" + } + + return "✅ COA exists at /storage/evm with type: ".concat(coaType!.identifier) +} \ No newline at end of file diff --git a/cadence/scripts/check_handler_paused.cdc b/cadence/scripts/check_handler_paused.cdc new file mode 100644 index 0000000..6bcddce --- /dev/null +++ b/cadence/scripts/check_handler_paused.cdc @@ -0,0 +1,6 @@ +import "FlowVaultsTransactionHandler" + +/// Check if the transaction handler is paused +access(all) fun main(): Bool { + return FlowVaultsTransactionHandler.isPausedState() +} diff --git a/cadence/scripts/check_pending_requests.cdc b/cadence/scripts/check_pending_requests.cdc new file mode 100644 index 0000000..e2f7708 --- /dev/null +++ b/cadence/scripts/check_pending_requests.cdc @@ -0,0 +1,23 @@ +import "FlowVaultsEVM" + +access(all) fun main(contractAddr: Address): Int { + let account = getAuthAccount(contractAddr) + + let worker = account.storage.borrow<&FlowVaultsEVM.Worker>( + from: FlowVaultsEVM.WorkerStoragePath + ) ?? panic("No Worker found") + + let requests = worker.getPendingRequestsFromEVM() + + log("Found ".concat(requests.length.toString()).concat(" pending requests")) + + for request in requests { + log("Request ID: ".concat(request.id.toString())) + log(" Type: ".concat(request.requestType.toString())) + log(" User: ".concat(request.user.toString())) + log(" Amount: ".concat(request.amount.toString())) + log(" Status: ".concat(request.status.toString())) + } + + return requests.length +} \ No newline at end of file diff --git a/cadence/scripts/check_public_coa.cdc b/cadence/scripts/check_public_coa.cdc new file mode 100644 index 0000000..e7ee4b9 --- /dev/null +++ b/cadence/scripts/check_public_coa.cdc @@ -0,0 +1,32 @@ +import "EVM" + +access(all) fun main(address: Address): AnyStruct { + let account = getAccount(address) + let publicPath = /public/evm + + let result: {String: AnyStruct} = {} + + // Check with the expected type first + let evmCap = account.capabilities.get<&EVM.CadenceOwnedAccount>(publicPath) + result["evmCapExists"] = evmCap != nil + result["evmCapValid"] = evmCap.check() + + if let evmRef = evmCap.borrow() { + result["coaAddress"] = evmRef.address().toString() + result["coaBalance"] = evmRef.balance().inAttoFLOW() + result["type"] = "Valid EVM.CadenceOwnedAccount" + } else { + // Try as generic AnyStruct to see if SOMETHING exists + let anyCap = account.capabilities.get<&AnyStruct>(publicPath) + result["anyCapExists"] = anyCap != nil + result["anyCapValid"] = anyCap.check() + + if anyCap.check() { + result["type"] = "Valid capability but not EVM.CadenceOwnedAccount type" + } else { + result["type"] = "Broken/invalid capability at path" + } + } + + return result +} \ No newline at end of file diff --git a/cadence/scripts/check_storage.cdc b/cadence/scripts/check_storage.cdc new file mode 100644 index 0000000..471e457 --- /dev/null +++ b/cadence/scripts/check_storage.cdc @@ -0,0 +1,13 @@ +// check_storage.cdc +access(all) fun main(address: Address): [String] { + let account = getAccount(address) + var paths: [String] = [] + + // Iterate through storage + account.storage.forEachStored(fun (path: StoragePath, type: Type): Bool { + paths.append(path.toString().concat(" -> ").concat(type.identifier)) + return true + }) + + return paths +} \ No newline at end of file diff --git a/cadence/scripts/check_tide_details.cdc b/cadence/scripts/check_tide_details.cdc new file mode 100644 index 0000000..9f4e5fe --- /dev/null +++ b/cadence/scripts/check_tide_details.cdc @@ -0,0 +1,53 @@ +// check_tide_details.cdc +import "FlowVaults" +import "FlowVaultsEVM" +import "DeFiActions" + +/// Script to get detailed information about specific Tides in the Worker's TideManager +/// +/// @param account: The account address where FlowVaultsEVM Worker is stored +/// @return Dictionary with comprehensive Tide details +/// +access(all) fun main(account: Address): {String: AnyStruct} { + let result: {String: AnyStruct} = {} + + // Get contract-level information + result["contractAddress"] = account.toString() + result["flowVaultsRequestsAddress"] = FlowVaultsEVM.getFlowVaultsRequestsAddress()?.toString() ?? "not set" + + // Get all EVM address mappings from FlowVaultsEVM + let tidesByEVM= FlowVaultsEVM.tidesByEVMAddress + result["totalEVMAddresses"] = tidesByEVM.keys.length + + let allTideIds: [UInt64] = [] + let evmMappings: [{String: AnyStruct}] = [] + + for evmAddr in tidesByEVM.keys { + let tides = FlowVaultsEVM.getTideIDsForEVMAddress(evmAddr) + allTideIds.appendAll(tides) + + evmMappings.append({ + "evmAddress": evmAddr, + "tideIds": tides, + "tideCount": tides.length + }) + } + + result["evmMappings"] = evmMappings + result["totalMappedTides"] = allTideIds.length + result["allMappedTideIds"] = allTideIds + + // Note: Cannot access TideManager directly from script as it's private in Worker + result["note"] = "TideManager is embedded in Worker resource - detailed Tide info requires transaction access" + + // Get supported strategies from FlowVaults + let strategies = FlowVaults.getSupportedStrategies() + let strategyIdentifiers: [String] = [] + for strategy in strategies { + strategyIdentifiers.append(strategy.identifier) + } + result["supportedStrategies"] = strategyIdentifiers + result["totalStrategies"] = strategies.length + + return result +} \ No newline at end of file diff --git a/cadence/scripts/check_tidemanager_status.cdc b/cadence/scripts/check_tidemanager_status.cdc new file mode 100644 index 0000000..eae3131 --- /dev/null +++ b/cadence/scripts/check_tidemanager_status.cdc @@ -0,0 +1,179 @@ +// check_tidemanager_status.cdc +import "FlowVaults" +import "FlowVaultsEVM" + +/// Script to get comprehensive TideManager status and health check +/// +/// @param account: The account address where Worker is stored +/// @return Dictionary with TideManager status and diagnostics +/// +access(all) fun main(accountAddress: Address): {String: AnyStruct} { + let result: {String: AnyStruct} = {} + let account = getAccount(accountAddress) + + // === Contract Configuration === + result["contractAddress"] = accountAddress.toString() + result["flowVaultsRequestsAddress"] = FlowVaultsEVM.getFlowVaultsRequestsAddress()?.toString() ?? "not set" + + // === Storage Paths === + let paths: {String: String} = {} + paths["workerStorage"] = FlowVaultsEVM.WorkerStoragePath.toString() + paths["workerPublic"] = FlowVaultsEVM.WorkerPublicPath.toString() + paths["adminStorage"] = FlowVaultsEVM.AdminStoragePath.toString() + paths["tideManagerStorage"] = FlowVaults.TideManagerStoragePath.toString() + paths["tideManagerPublic"] = FlowVaults.TideManagerPublicPath.toString() + result["paths"] = paths + + // === EVM Address Mappings === + let tidesByEVM = FlowVaultsEVM.tidesByEVMAddress + result["totalEVMAddresses"] = tidesByEVM.keys.length + + var totalTidesMapped = 0 + let evmDetails: [{String: AnyStruct}] = [] + + for evmAddr in tidesByEVM.keys { + let tides = FlowVaultsEVM.getTideIDsForEVMAddress(evmAddr) + totalTidesMapped = totalTidesMapped + tides.length + + evmDetails.append({ + "evmAddress": "0x".concat(evmAddr), + "tideCount": tides.length, + "tideIds": tides + }) + } + + result["evmAddressDetails"] = evmDetails + result["totalMappedTides"] = totalTidesMapped + + // === Strategy Information === + let strategies = FlowVaults.getSupportedStrategies() + let strategyInfo: [{String: AnyStruct}] = [] + + for strategy in strategies { + let initVaults = FlowVaults.getSupportedInitializationVaults(forStrategy: strategy) + + let vaultTypes: [String] = [] + for vaultType in initVaults.keys { + if initVaults[vaultType]! { + vaultTypes.append(vaultType.identifier) + } + } + + strategyInfo.append({ + "strategyType": strategy.identifier, + "supportedInitVaults": vaultTypes, + "vaultCount": vaultTypes.length + }) + } + + result["strategies"] = strategyInfo + result["totalStrategies"] = strategies.length + + // === Storage Inspection === + let storagePaths: [String] = [] + account.storage.forEachStored(fun (path: StoragePath, type: Type): Bool { + storagePaths.append(path.toString().concat(" -> ").concat(type.identifier)) + return true + }) + result["storagePaths"] = storagePaths + result["storageItemCount"] = storagePaths.length + + // === Public Capabilities === + let publicPaths: [String] = [] + + let knownPublicPaths: [PublicPath] = [ + FlowVaultsEVM.WorkerPublicPath, + FlowVaults.TideManagerPublicPath + ] + + for publicPath in knownPublicPaths { + let cap = account.capabilities.get<&AnyResource>(publicPath) + if cap != nil { + publicPaths.append(publicPath.toString()) + } + } + + result["publicPaths"] = publicPaths + result["publicCapabilityCount"] = publicPaths.length + + + // === Health Checks === + let healthChecks: {String: String} = {} + + // Check FlowVaultsRequests address + if FlowVaultsEVM.getFlowVaultsRequestsAddress() != nil { + healthChecks["flowVaultsRequestsAddress"] = "✅ SET" + } else { + healthChecks["flowVaultsRequestsAddress"] = "❌ NOT SET" + } + + // Check strategies + if strategies.length > 0 { + healthChecks["strategies"] = "✅ ".concat(strategies.length.toString()).concat(" available") + } else { + healthChecks["strategies"] = "❌ NO STRATEGIES" + } + + // Check Worker exists (look for Worker in storage paths) + var workerExists = false + for path in storagePaths { + if path.contains("FlowVaultsEVM.Worker") { + workerExists = true + break + } + } + + if workerExists { + healthChecks["worker"] = "✅ EXISTS" + } else { + healthChecks["worker"] = "❌ NOT FOUND" + } + + // Check EVM users + if tidesByEVM.keys.length > 0 { + healthChecks["evmUsers"] = "✅ ".concat(tidesByEVM.keys.length.toString()).concat(" registered") + } else { + healthChecks["evmUsers"] = "⚠️ NO USERS YET" + } + + // Check Tides + if totalTidesMapped > 0 { + healthChecks["tides"] = "✅ ".concat(totalTidesMapped.toString()).concat(" created") + } else { + healthChecks["tides"] = "⚠️ NO TIDES YET" + } + + result["healthChecks"] = healthChecks + + // === Overall Status === + let criticalChecks = FlowVaultsEVM.getFlowVaultsRequestsAddress() != nil && strategies.length > 0 && workerExists + + if criticalChecks && totalTidesMapped > 0 { + result["status"] = "🟢 OPERATIONAL" + result["statusMessage"] = "Bridge is operational with active Tides" + } else if criticalChecks { + result["status"] = "🟡 READY" + result["statusMessage"] = "Bridge is configured but no Tides created yet" + } else { + result["status"] = "🔴 NEEDS CONFIGURATION" + result["statusMessage"] = "Critical components missing" + } + + // === Summary === + result["summary"] = { + "evmUsers": tidesByEVM.keys.length, + "totalTides": totalTidesMapped, + "strategies": strategies.length, + "storageItems": storagePaths.length, + "publicCapabilities": publicPaths.length + } + + // === Notes === + result["notes"] = [ + "TideManager is embedded inside Worker resource (private access)", + "Detailed Tide information requires transaction-based inspection", + "EVM bridge status depends on COA authorization in Solidity contract" + ] + + return result +} \ No newline at end of file diff --git a/cadence/scripts/check_user_tides.cdc b/cadence/scripts/check_user_tides.cdc new file mode 100644 index 0000000..79caa57 --- /dev/null +++ b/cadence/scripts/check_user_tides.cdc @@ -0,0 +1,31 @@ +// check_user_tides.cdc +import "FlowVaultsEVM" + +/// Script to check what Tide IDs are associated with an EVM address +/// +/// @param evmAddress: The EVM address (as hex string with or without 0x prefix) +/// @return Array of Tide IDs owned by this EVM user +/// +access(all) fun main(evmAddress: String): [UInt64] { + // Normalize address (remove 0x prefix if present, convert to lowercase) + var normalizedAddress = evmAddress.toLower() + if normalizedAddress.length > 2 && normalizedAddress.slice(from: 0, upTo: 2) == "0x" { + normalizedAddress = normalizedAddress.slice(from: 2, upTo: normalizedAddress.length) + } + + // Pad to 40 characters (20 bytes) if needed + while normalizedAddress.length < 40 { + normalizedAddress = "0".concat(normalizedAddress) + } + + log("Checking Tides for EVM address: ".concat(normalizedAddress)) + + let tideIds = FlowVaultsEVM.getTideIDsForEVMAddress(normalizedAddress) + + log("Found ".concat(tideIds.length.toString()).concat(" Tide(s)")) + for id in tideIds { + log(" - Tide ID: ".concat(id.toString())) + } + + return tideIds +} diff --git a/cadence/scripts/get_coa_address.cdc b/cadence/scripts/get_coa_address.cdc new file mode 100644 index 0000000..d35a10c --- /dev/null +++ b/cadence/scripts/get_coa_address.cdc @@ -0,0 +1,12 @@ +import "EVM" + +access(all) fun main(account: Address): String { + let acct = getAuthAccount(account) + + // Borrow the COA from the standard EVM storage path + let coa = acct.storage.borrow<&EVM.CadenceOwnedAccount>( + from: /storage/evm + ) ?? panic("COA not found at /storage/evm") + + return coa.address().toString() +} \ No newline at end of file diff --git a/cadence/scripts/get_contract_state.cdc b/cadence/scripts/get_contract_state.cdc new file mode 100644 index 0000000..8fc2bda --- /dev/null +++ b/cadence/scripts/get_contract_state.cdc @@ -0,0 +1,35 @@ +import "FlowVaultsEVM" + +access(all) fun main(contractAddress: Address): {String: AnyStruct} { + let result: {String: AnyStruct} = {} + + // Get all public state variables + result["flowVaultsRequestsAddress"] = FlowVaultsEVM.getFlowVaultsRequestsAddress()?.toString() ?? "Not set" + result["tidesByEVMAddress"] = FlowVaultsEVM.tidesByEVMAddress + + // Get all public paths + result["WorkerStoragePath"] = FlowVaultsEVM.WorkerStoragePath.toString() + result["WorkerPublicPath"] = FlowVaultsEVM.WorkerPublicPath.toString() + result["AdminStoragePath"] = FlowVaultsEVM.AdminStoragePath.toString() + + // Count total tides across all EVM addresses + var totalTides = 0 + var totalEVMAddresses = 0 + for evmAddress in FlowVaultsEVM.tidesByEVMAddress.keys { + totalEVMAddresses = totalEVMAddresses + 1 + let tideIds = FlowVaultsEVM.tidesByEVMAddress[evmAddress]! + totalTides = totalTides + tideIds.length + } + + result["totalEVMAddresses"] = totalEVMAddresses + result["totalTides"] = totalTides + + // List all EVM addresses with their tide counts + let evmAddressDetails: {String: Int} = {} + for evmAddress in FlowVaultsEVM.tidesByEVMAddress.keys { + evmAddressDetails[evmAddress] = FlowVaultsEVM.tidesByEVMAddress[evmAddress]!.length + } + result["evmAddressDetails"] = evmAddressDetails + + return result +} \ No newline at end of file diff --git a/cadence/scripts/get_max_requests_config.cdc b/cadence/scripts/get_max_requests_config.cdc new file mode 100644 index 0000000..e328842 --- /dev/null +++ b/cadence/scripts/get_max_requests_config.cdc @@ -0,0 +1,55 @@ +import "FlowVaultsEVM" + +/// Get the current MAX_REQUESTS_PER_TX value and related statistics +/// +/// This helps you understand current batch processing configuration +/// and make informed decisions about tuning +/// +access(all) fun main(): {String: AnyStruct} { + let maxRequestsPerTx = FlowVaultsEVM.MAX_REQUESTS_PER_TX + + // Calculate some helpful metrics + let executionsPerHourAt5s = 720 + let executionsPerHourAt60s = 60 + + let maxThroughputPerHour5s = maxRequestsPerTx * executionsPerHourAt5s + let maxThroughputPerHour60s = maxRequestsPerTx * executionsPerHourAt60s + + return { + "currentMaxRequestsPerTx": maxRequestsPerTx, + "maxThroughputPerHour": { + "at5sDelay": maxThroughputPerHour5s, + "at60sDelay": maxThroughputPerHour60s + }, + "estimatedGasPerExecution": { + "description": "Varies based on request complexity", + "rangePerRequest": "~100k-500k gas", + "totalRange": calculateGasRange(maxRequestsPerTx) + }, + "recommendations": getRecommendations(maxRequestsPerTx) + } +} + +access(all) fun calculateGasRange(_ batchSize: Int): String { + let lowGas = batchSize * 100_000 + let highGas = batchSize * 500_000 + return lowGas.toString().concat(" - ").concat(highGas.toString()).concat(" gas") +} + +access(all) fun getRecommendations(_ current: Int): [String] { + let recommendations: [String] = [] + + if current < 5 { + recommendations.append("⚠️ Very small batch size - consider increasing for efficiency") + } else if current < 10 { + recommendations.append("✅ Conservative batch size - good for testing") + } else if current <= 30 { + recommendations.append("✅ Optimal batch size range") + } else if current <= 50 { + recommendations.append("⚠️ Large batch size - monitor for gas issues") + } else { + recommendations.append("🚨 Very large batch size - high risk of gas limits") + } + + return recommendations +} diff --git a/cadence/scripts/get_request_details.cdc b/cadence/scripts/get_request_details.cdc new file mode 100644 index 0000000..0f63412 --- /dev/null +++ b/cadence/scripts/get_request_details.cdc @@ -0,0 +1,31 @@ +import "FlowVaultsEVM" + +access(all) fun main(contractAddr: Address): {String: AnyStruct} { + let account = getAuthAccount(contractAddr) + + let worker = account.storage.borrow<&FlowVaultsEVM.Worker>( + from: FlowVaultsEVM.WorkerStoragePath + ) ?? panic("No Worker found") + + let requests = worker.getPendingRequestsFromEVM() + + if requests.length == 0 { + return {"message": "No pending requests"} + } + + let request = requests[0] + + return { + "id": request.id.toString(), + "user": request.user.toString(), + "requestType": request.requestType, + "requestTypeName": request.requestType == 0 ? "CREATE_TIDE" : (request.requestType == 3 ? "CLOSE_TIDE" : "UNKNOWN"), + "status": request.status, + "statusName": request.status == 0 ? "PENDING" : (request.status == 1 ? "COMPLETED" : "FAILED"), + "tokenAddress": request.tokenAddress.toString(), + "amount": request.amount.toString(), + "tideId": request.tideId.toString(), + "timestamp": request.timestamp.toString(), + "message": request.message + } +} \ No newline at end of file diff --git a/cadence/transactions/check_delay_for_pending_count.cdc b/cadence/transactions/check_delay_for_pending_count.cdc new file mode 100644 index 0000000..2a3d840 --- /dev/null +++ b/cadence/transactions/check_delay_for_pending_count.cdc @@ -0,0 +1,51 @@ +import "FlowVaultsTransactionHandler" + +/// Query what delay would be used for a given number of pending requests +/// Useful for understanding and testing the smart scheduling algorithm +/// +/// Arguments: +/// - pendingRequests: Number of pending requests to check +/// +/// Returns: +/// - delayLevel: Index in DELAY_LEVELS array (0-9) +/// - delaySeconds: Delay that would be used +/// - description: Human-readable description +/// +access(all) fun main(pendingRequests: Int): {String: AnyStruct} { + let delayLevel = FlowVaultsTransactionHandler.getDelayLevel(pendingRequests) + let delay = FlowVaultsTransactionHandler.DELAY_LEVELS[delayLevel] + + return { + "pendingRequests": pendingRequests, + "delayLevel": delayLevel, + "delaySeconds": delay, + "description": getDescription(delayLevel), + "loadCategory": getLoadCategory(delayLevel) + } +} + +access(all) fun getDescription(_ level: Int): String { + switch level { + case 0: return "Extreme Load - Process every 5 seconds" + case 1: return "Very High Load - Process every 10 seconds" + case 2: return "High Load - Process every 15 seconds" + case 3: return "Medium-High Load - Process every 20 seconds" + case 4: return "Medium Load - Process every 30 seconds" + case 5: return "Medium-Low Load - Process every 45 seconds" + case 6: return "Low Load - Process every 60 seconds" + case 7: return "Very Low Load - Process every 60 seconds" + case 8: return "Minimal Load - Process every 60 seconds" + case 9: return "Idle - Process every 60 seconds" + default: return "Unknown" + } +} + +access(all) fun getLoadCategory(_ level: Int): String { + if level <= 2 { + return "HIGH" + } else if level <= 5 { + return "MEDIUM" + } else { + return "LOW" + } +} diff --git a/cadence/transactions/fund_evm_from_coa.cdc b/cadence/transactions/fund_evm_from_coa.cdc new file mode 100644 index 0000000..b169428 --- /dev/null +++ b/cadence/transactions/fund_evm_from_coa.cdc @@ -0,0 +1,52 @@ +import "EVM" +import "FlowToken" +import "FungibleToken" + +/// Transfers FLOW from Cadence account to an EVM address via COA +/// @param evmAddressHex: The hex address of the EVM account to fund (without 0x prefix) +/// @param amount: Amount of FLOW to transfer +transaction(evmAddressHex: String, amount: UFix64) { + let coa: auth(EVM.Call) &EVM.CadenceOwnedAccount + let sentVault: @FlowToken.Vault + + prepare(signer: auth(BorrowValue) &Account) { + // Borrow COA reference with Call entitlement + self.coa = signer.storage.borrow( + from: /storage/evm + ) ?? panic("Could not borrow COA reference") + + // Withdraw FLOW from signer's vault + let vaultRef = signer.storage.borrow( + from: /storage/flowTokenVault + ) ?? panic("Could not borrow Flow vault reference") + + self.sentVault <- vaultRef.withdraw(amount: amount) as! @FlowToken.Vault + } + + execute { + // First, deposit the FLOW into the COA + self.coa.deposit(from: <-self.sentVault) + + // Convert the target EVM address from hex + let toAddress = EVM.addressFromString(evmAddressHex) + + // Calculate amount in attoflow (1 FLOW = 1e18 on EVM, but UFix64 in Cadence has 8 decimals) + // So we multiply by 1e8 first (which UFix64 can handle), then by 1e10 to reach 1e18 + let amountInAttoflow = UInt(amount * 100_000_000.0) * 10_000_000_000 + + // Transfer from COA to target EVM address + let result = self.coa.call( + to: toAddress, + data: [], // empty data for simple transfer + gasLimit: 100_000, + value: EVM.Balance(attoflow: amountInAttoflow) + ) + + // Ensure transfer was successful + assert( + result.status == EVM.Status.successful, + message: "Transfer failed with error code: ".concat(result.errorCode.toString()) + .concat(" - ").concat(result.errorMessage) + ) + } +} \ No newline at end of file diff --git a/cadence/transactions/init_flow_vaults_transaction_handler.cdc b/cadence/transactions/init_flow_vaults_transaction_handler.cdc new file mode 100644 index 0000000..7630c40 --- /dev/null +++ b/cadence/transactions/init_flow_vaults_transaction_handler.cdc @@ -0,0 +1,53 @@ +import "FlowVaultsTransactionHandler" +import "FlowTransactionScheduler" +import "FlowVaultsEVM" + +/// Initialize the FlowVaultsTransactionHandler +/// This should be run once after FlowVaultsEVM Worker is set up +/// +/// This transaction: +/// 1. Creates a capability to the FlowVaultsEVM Worker +/// 2. Creates and saves the Handler resource +/// 3. Issues both entitled and public capabilities for the handler +/// +transaction() { + prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, SaveValue, PublishCapability) &Account) { + log("=== Initializing FlowVaultsTransactionHandler ===") + + // Check if Worker exists + if signer.storage.borrow<&FlowVaultsEVM.Worker>(from: FlowVaultsEVM.WorkerStoragePath) == nil { + panic("FlowVaultsEVM Worker not found. Please initialize Worker first.") + } + + // Create a capability to the Worker + let workerCap = signer.capabilities.storage + .issue<&FlowVaultsEVM.Worker>(FlowVaultsEVM.WorkerStoragePath) + + log("Worker capability created") + + // Create and save the handler with the worker capability + if signer.storage.borrow<&AnyResource>(from: FlowVaultsTransactionHandler.HandlerStoragePath) == nil { + let handler <- FlowVaultsTransactionHandler.createHandler(workerCap: workerCap) + signer.storage.save(<-handler, to: FlowVaultsTransactionHandler.HandlerStoragePath) + log("Handler resource saved to storage") + } else { + log("Handler already exists in storage") + } + + // Issue an entitled capability for the scheduler to call executeTransaction - VALIDATION for future calls + let entitledCap = signer.capabilities.storage + .issue( + FlowVaultsTransactionHandler.HandlerStoragePath + ) + + // Issue a public capability for general access + let publicCap = signer.capabilities.storage + .issue<&{FlowTransactionScheduler.TransactionHandler}>( + FlowVaultsTransactionHandler.HandlerStoragePath + ) + signer.capabilities.publish(publicCap, at: FlowVaultsTransactionHandler.HandlerPublicPath) + log("Public handler capability published") + + log("=== FlowVaultsTransactionHandler Initialization Complete ===") + } +} diff --git a/cadence/transactions/pause_transaction_handler.cdc b/cadence/transactions/pause_transaction_handler.cdc new file mode 100644 index 0000000..e85745d --- /dev/null +++ b/cadence/transactions/pause_transaction_handler.cdc @@ -0,0 +1,18 @@ +import "FlowVaultsTransactionHandler" + +/// Pause the automated transaction handler +/// When paused, scheduled executions will run but skip processing +/// and will NOT schedule the next execution, breaking the chain +transaction() { + prepare(signer: auth(BorrowValue) &Account) { + let admin = signer.storage.borrow<&FlowVaultsTransactionHandler.Admin>( + from: FlowVaultsTransactionHandler.AdminStoragePath + ) ?? panic("Could not borrow Admin resource") + + admin.pause() + + log("✅ Handler paused successfully") + log("⚠️ Currently scheduled transactions will still execute") + log("⚠️ But they will skip processing and NOT schedule the next execution") + } +} diff --git a/cadence/transactions/process_requests.cdc b/cadence/transactions/process_requests.cdc new file mode 100644 index 0000000..09ed68d --- /dev/null +++ b/cadence/transactions/process_requests.cdc @@ -0,0 +1,25 @@ +// process_requests.cdc +import "FlowVaultsEVM" + +/// Transaction to process all pending requests from FlowVaultsRequests contract +/// This will create Tides for any pending CREATE_TIDE requests +/// +/// Run this after users have created requests on the EVM side +/// +transaction() { + prepare(signer: auth(BorrowValue) &Account) { + + // Borrow the Worker from storage + let worker = signer.storage.borrow<&FlowVaultsEVM.Worker>( + from: FlowVaultsEVM.WorkerStoragePath + ) ?? panic("Could not borrow Worker from storage") + + log("=== Processing Pending Requests ===") + log("Worker COA address: ".concat(worker.getCOAAddressString())) + + // Process all pending requests + worker.processRequests() + + log("=== Processing Complete ===") + } +} diff --git a/cadence/transactions/schedule_initial_flow_vaults_execution.cdc b/cadence/transactions/schedule_initial_flow_vaults_execution.cdc new file mode 100644 index 0000000..0d2165e --- /dev/null +++ b/cadence/transactions/schedule_initial_flow_vaults_execution.cdc @@ -0,0 +1,114 @@ +import "FlowTransactionScheduler" +import "FlowTransactionSchedulerUtils" +import "FlowToken" +import "FungibleToken" +import "FlowVaultsTransactionHandler" +import "FlowVaultsEVM" + +/// Schedule the first FlowVaultsEVM request processing execution +/// After this, the handler will automatically schedule subsequent executions +/// based on the smart scheduling algorithm +/// +/// Arguments: +/// - delaySeconds: Initial delay before first execution (e.g., 5.0 for 5 seconds) +/// - priority: 0=High, 1=Medium, 2=Low (recommend Medium for automated processing) +/// - executionEffort: Computation units (recommend 6000+ for safety) +/// +transaction( + delaySeconds: UFix64, + priority: UInt8, + executionEffort: UInt64 +) { + prepare(signer: auth(BorrowValue, IssueStorageCapabilityController, SaveValue, GetStorageCapabilityController, PublishCapability) &Account) { + log("=== Scheduling Initial FlowVaultsEVM Execution ===") + + // Calculate future timestamp + let future = getCurrentBlock().timestamp + delaySeconds + log("Scheduled for: ".concat(future.toString())) + log("Delay: ".concat(delaySeconds.toString()).concat(" seconds")) + + // Convert priority + let pr = priority == 0 + ? FlowTransactionScheduler.Priority.High + : priority == 1 + ? FlowTransactionScheduler.Priority.Medium + : FlowTransactionScheduler.Priority.Low + log("Priority: ".concat(pr.rawValue.toString())) + + // Get the entitled handler capability + var handlerCap: Capability? = nil + let controllers = signer.capabilities.storage.getControllers(forPath: FlowVaultsTransactionHandler.HandlerStoragePath) + + for controller in controllers { + if let cap = controller.capability as? Capability { + handlerCap = cap + break + } + } + + if handlerCap == nil { + panic("Could not find entitled handler capability. Please run InitFlowVaultsTransactionHandler.cdc first.") + } + log("Handler capability found") + + // Initialize scheduler manager if not present + if signer.storage.borrow<&AnyResource>(from: FlowTransactionSchedulerUtils.managerStoragePath) == nil { + log("Creating new scheduler manager") + let manager <- FlowTransactionSchedulerUtils.createManager() + signer.storage.save(<-manager, to: FlowTransactionSchedulerUtils.managerStoragePath) + + let managerCapPublic = signer.capabilities.storage + .issue<&{FlowTransactionSchedulerUtils.Manager}>(FlowTransactionSchedulerUtils.managerStoragePath) + signer.capabilities.publish(managerCapPublic, at: FlowTransactionSchedulerUtils.managerPublicPath) + log("Scheduler manager created and published") + } + + // Borrow the manager + let manager = signer.storage + .borrow( + from: FlowTransactionSchedulerUtils.managerStoragePath + ) ?? panic("Could not borrow Manager reference") + log("Manager borrowed successfully") + + // Estimate fees + log("Estimating transaction fees...") + let est = FlowTransactionScheduler.estimate( + data: nil, + timestamp: future, + priority: pr, + executionEffort: executionEffort + ) + + let estimatedFee = est.flowFee ?? 0.0 + log("Estimated fee: ".concat(estimatedFee.toString()).concat(" FLOW")) + + if est.timestamp == nil && pr != FlowTransactionScheduler.Priority.Low { + let errorMsg = est.error ?? "estimation failed" + panic("Fee estimation failed: ".concat(errorMsg)) + } + + // Withdraw fees from vault + let vaultRef = signer.storage + .borrow(from: /storage/flowTokenVault) + ?? panic("Missing FlowToken vault") + + let fees <- vaultRef.withdraw(amount: estimatedFee) as! @FlowToken.Vault + log("Fees withdrawn from vault") + + // Schedule the transaction + let transactionId = manager.schedule( + handlerCap: handlerCap!, + data: nil, + timestamp: future, + priority: pr, + executionEffort: executionEffort, + fees: <-fees + ) + + log("✅ Scheduled transaction ID: ".concat(transactionId.toString())) + log("Execution will trigger at: ".concat(future.toString())) + log("After execution, the handler will automatically schedule the next run") + log("based on the number of pending requests (smart scheduling)") + log("=== Scheduling Complete ===") + } +} diff --git a/cadence/transactions/setup_coa.cdc b/cadence/transactions/setup_coa.cdc new file mode 100644 index 0000000..156e40a --- /dev/null +++ b/cadence/transactions/setup_coa.cdc @@ -0,0 +1,27 @@ +import "EVM" + +transaction() { + prepare(signer: auth(SaveValue, IssueStorageCapabilityController, PublishCapability, BorrowValue) &Account) { + let storagePath = /storage/evm + let publicPath = /public/evm + + // Check if COA already exists + if signer.storage.borrow<&EVM.CadenceOwnedAccount>(from: storagePath) == nil { + // Create account & save to storage + let coa: @EVM.CadenceOwnedAccount <- EVM.createCadenceOwnedAccount() + log("COA Address: ".concat(coa.address().toString())) + signer.storage.save(<-coa, to: storagePath) + + // Publish a public capability to the COA + let cap = signer.capabilities.storage.issue<&EVM.CadenceOwnedAccount>(storagePath) + signer.capabilities.publish(cap, at: publicPath) + } else { + log("Cadence Owned Account already exists at the specified storage path.") + // Borrow a reference to the COA from the storage location we saved it to + let coa = signer.storage.borrow<&EVM.CadenceOwnedAccount>(from: storagePath) ?? + panic("Could not borrow reference to the signer's CadenceOwnedAccount (COA). " + .concat("Ensure the signer account has a COA stored in the canonical /storage/evm path")) + log("COA Address: ".concat(coa.address().toString())) + } + } +} \ No newline at end of file diff --git a/cadence/transactions/setup_worker_with_badge.cdc b/cadence/transactions/setup_worker_with_badge.cdc new file mode 100644 index 0000000..a20d5d3 --- /dev/null +++ b/cadence/transactions/setup_worker_with_badge.cdc @@ -0,0 +1,94 @@ +import "FlowVaultsEVM" +import "FlowVaultsClosedBeta" +import "EVM" + +/// Setup Worker transaction for FlowVaultsEVM Intermediate Package +/// Handles both new beta badge creation and existing beta badge usage +/// +/// @param flowVaultsRequestsAddress: The EVM address of the FlowVaultsRequests contract +/// +transaction(flowVaultsRequestsAddress: String) { + prepare(signer: auth(BorrowValue, SaveValue, LoadValue, Storage, Capabilities, CopyValue, IssueStorageCapabilityController) &Account) { + + log("=== Starting FlowVaultsEVM Worker Setup ===") + + // ======================================== + // Step 1: Get or create beta badge capability + // ======================================== + + var betaBadgeCap: Capability? = nil + + // First, try to find existing beta badge in standard storage path + let standardStoragePath = FlowVaultsClosedBeta.UserBetaCapStoragePath + if signer.storage.type(at: standardStoragePath) != nil { + betaBadgeCap = signer.storage.copy>( + from: standardStoragePath + ) + log("✓ Using existing beta badge capability from standard path") + } + + // If not found in standard path, try the specific user path + if betaBadgeCap == nil { + let userSpecificPath = /storage/FlowVaultsUserBetaCap_0x3bda2f90274dbc9b + if signer.storage.type(at: userSpecificPath) != nil { + betaBadgeCap = signer.storage.copy>( + from: userSpecificPath + ) + log("✓ Using existing beta badge capability from user-specific path") + } + } + + // If still no beta badge found, create a new one (requires Admin) + if betaBadgeCap == nil { + log("• No existing beta badge found. Granting new beta badge...") + + let betaAdminHandle = signer.storage.borrow( + from: FlowVaultsClosedBeta.AdminHandleStoragePath + ) ?? panic("Could not borrow AdminHandle - you need admin access or an existing beta badge") + + betaBadgeCap = betaAdminHandle.grantBeta(addr: signer.address) + signer.storage.save(betaBadgeCap!, to: standardStoragePath) + log("✓ Beta badge capability created and saved") + } + + // Verify the capability is valid + let betaRef = betaBadgeCap!.borrow() + ?? panic("Beta badge capability does not contain correct reference") + log("✓ Beta badge verified for address: ".concat(betaRef.getOwner().toString())) + + // ======================================== + // Step 2: Setup COA capability + // ======================================== + + let admin = signer.storage.borrow<&FlowVaultsEVM.Admin>( + from: FlowVaultsEVM.AdminStoragePath + ) ?? panic("Could not borrow FlowVaultsEVM Admin") + + // Issue a storage capability to the COA at /storage/evm + let coaCap = signer.capabilities.storage.issue( + /storage/evm + ) + + // Verify the capability works + let coaRef = coaCap.borrow() + ?? panic("Could not borrow COA capability from /storage/evm") + log("✓ Using COA with address: ".concat(coaRef.address().toString())) + + // Create worker with the COA capability and beta badge capability + let worker <- admin.createWorker(coaCap: coaCap, betaBadgeCap: betaBadgeCap!) + + // Save worker to storage + signer.storage.save(<-worker, to: FlowVaultsEVM.WorkerStoragePath) + log("✓ Worker created and saved to storage") + + // ======================================== + // Step 3: Set FlowVaultsRequests Contract Address + // ======================================== + + let evmAddress = EVM.addressFromString(flowVaultsRequestsAddress) + admin.setFlowVaultsRequestsAddress(evmAddress) + log("✓ FlowVaultsRequests address set to: ".concat(flowVaultsRequestsAddress)) + + log("=== FlowVaultsEVM Worker Setup Complete ===") + } +} \ No newline at end of file diff --git a/cadence/transactions/unpause_transaction_handler.cdc b/cadence/transactions/unpause_transaction_handler.cdc new file mode 100644 index 0000000..44ee492 --- /dev/null +++ b/cadence/transactions/unpause_transaction_handler.cdc @@ -0,0 +1,19 @@ +import "FlowVaultsTransactionHandler" + +/// Unpause the automated transaction handler +/// After unpausing, you'll need to manually schedule a new execution +/// using schedule_initial_flow_vaults_execution.cdc +transaction() { + prepare(signer: auth(BorrowValue) &Account) { + let admin = signer.storage.borrow<&FlowVaultsTransactionHandler.Admin>( + from: FlowVaultsTransactionHandler.AdminStoragePath + ) ?? panic("Could not borrow Admin resource") + + admin.unpause() + + log("✅ Handler unpaused successfully") + log("📝 To resume automated processing, run:") + log(" flow transactions send cadence/transactions/schedule_initial_flow_vaults_execution.cdc \\") + log(" 10.0 1 7499 --network testnet --signer testnet-account") + } +} diff --git a/cadence/transactions/update_flow_vaults_requests_address.cdc b/cadence/transactions/update_flow_vaults_requests_address.cdc new file mode 100644 index 0000000..b95c55b --- /dev/null +++ b/cadence/transactions/update_flow_vaults_requests_address.cdc @@ -0,0 +1,15 @@ +import "FlowVaultsEVM" +import "EVM" + +transaction(newAddress: String) { + prepare(acct: auth(Storage) &Account) { + let admin = acct.storage.borrow<&FlowVaultsEVM.Admin>( + from: FlowVaultsEVM.AdminStoragePath + ) ?? panic("Could not borrow Admin resource") + + let evmAddress = EVM.addressFromString(newAddress) + admin.updateFlowVaultsRequestsAddress(evmAddress) // 👈 Nouvelle fonction + + log("FlowVaultsRequests address updated to: ".concat(newAddress)) + } +} \ No newline at end of file diff --git a/cadence/transactions/update_max_requests.cdc b/cadence/transactions/update_max_requests.cdc new file mode 100644 index 0000000..9c4334c --- /dev/null +++ b/cadence/transactions/update_max_requests.cdc @@ -0,0 +1,35 @@ +import "FlowVaultsEVM" + +/// Update the maximum number of requests to process per transaction +/// +/// Use this to tune performance based on gas benchmarking: +/// - Lower values: More predictable gas, slower throughput +/// - Higher values: Faster throughput, risk hitting gas limits +/// +/// Recommended range: 5-50 based on testing +/// +/// @param newMax: The new maximum requests per transaction +/// +transaction(newMax: Int) { + prepare(signer: auth(BorrowValue) &Account) { + + log("=== Updating MAX_REQUESTS_PER_TX ===") + log("Current value: ".concat(FlowVaultsEVM.MAX_REQUESTS_PER_TX.toString())) + log("New value: ".concat(newMax.toString())) + + // Borrow the Admin resource + let admin = signer.storage.borrow<&FlowVaultsEVM.Admin>( + from: FlowVaultsEVM.AdminStoragePath + ) ?? panic("Could not borrow FlowVaultsEVM Admin resource") + + // Update the value + admin.updateMaxRequestsPerTx(newMax) + + log("✅ MAX_REQUESTS_PER_TX updated successfully") + log("New value: ".concat(FlowVaultsEVM.MAX_REQUESTS_PER_TX.toString())) + } + + post { + FlowVaultsEVM.MAX_REQUESTS_PER_TX == newMax: "MAX_REQUESTS_PER_TX was not updated correctly" + } +} diff --git a/create_tide.png b/create_tide.png new file mode 100644 index 0000000..67639b5 Binary files /dev/null and b/create_tide.png differ diff --git a/deploy_testnet_full_stack.sh b/deploy_testnet_full_stack.sh new file mode 100755 index 0000000..bd6e6de --- /dev/null +++ b/deploy_testnet_full_stack.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# Full Stack Deployment and Setup Script for Flow Testnet +# This script: +# 1. Deploys Solidity contracts (FlowVaultsRequests) +# 2. Deploys Cadence contracts (FlowVaultsEVM, FlowVaultsTransactionHandler) +# 3. Updates FlowVaultsEVM with the deployed contract address +# 4. Initializes the transaction handler +# 5. Schedules the first automated execution + +set -e # Exit on any error + +echo "==========================================" +echo "🚀 Flow Vaults Full Stack Deployment" +echo " Network: Flow Testnet" +echo "==========================================" +echo "" + +# ========================================== +# Step 1: Deploy Solidity Contract +# ========================================== +echo "📦 Step 1: Deploying Solidity contracts..." +cd solidity +./script/deploy_and_verify.sh +cd .. + +# Extract deployed contract address from broadcast file +DEPLOYED_ADDRESS=$(jq -r '.transactions[0].contractAddress' solidity/broadcast/DeployFlowVaultsRequests.s.sol/545/run-latest.json) + +if [ -z "$DEPLOYED_ADDRESS" ] || [ "$DEPLOYED_ADDRESS" == "null" ]; then + echo "❌ Error: Could not find deployed contract address" + exit 1 +fi + +echo "" +echo "✅ FlowVaultsRequests deployed at: $DEPLOYED_ADDRESS" +echo "" + +# ========================================== +# Step 2: Deploy Cadence Contracts +# ========================================== +echo "📦 Step 2: Deploying Cadence contracts..." +flow project deploy -n=testnet --update + +echo "" +echo "✅ Cadence contracts deployed" +echo "" + +# ========================================== +# Step 3: Setup Worker with Badge +# ========================================== +echo "🔧 Step 3: Setting up Worker with Beta Badge and FlowVaultsRequests address..." +flow transactions send cadence/transactions/setup_worker_with_badge.cdc \ + $DEPLOYED_ADDRESS \ + --network testnet \ + --signer testnet-account --gas-limit 9999 + +echo "" +echo "✅ Worker initialized and FlowVaultsRequests address set" +echo "" + +# ========================================== +# Step 4: Initialize Transaction Handler +# ========================================== +echo "🔧 Step 4: Initializing FlowVaultsTransactionHandler..." +flow transactions send cadence/transactions/init_flow_vaults_transaction_handler.cdc \ + --network testnet \ + --signer testnet-account --gas-limit 9999 + +echo "" +echo "✅ Transaction Handler initialized" +echo "" + +# ========================================== +# Step 5: Schedule Initial Execution +# ========================================== +echo "⏰ Step 5: Scheduling initial automated execution..." +echo " - Delay: 10 seconds" +echo " - Priority: Medium (1)" +echo " - Execution Effort: 7499" + +flow transactions send cadence/transactions/schedule_initial_flow_vaults_execution.cdc \ + 10.0 1 7499 \ + --network testnet \ + --signer testnet-account --gas-limit 9999 + +echo "" +echo "✅ Initial execution scheduled" +echo "" + +# ========================================== +# Deployment Summary +# ========================================== +echo "==========================================" +echo "🎉 Full Stack Deployment Complete!" +echo "==========================================" +echo "" +echo "📋 Deployment Summary:" +echo " EVM Contract: $DEPLOYED_ADDRESS" +echo " Cadence Contracts: Deployed to testnet-account" +echo " Transaction Handler: Initialized" +echo " Scheduled Execution: Active (60s delay)" +echo "" +echo "🔗 View EVM Contract:" +echo " https://evm-testnet.flowscan.io/address/$DEPLOYED_ADDRESS" +echo "" +echo "📝 Next Steps:" +echo " 1. Monitor transaction handler execution" +echo " 2. Check pending requests processing" +echo " 3. Verify automated scheduling is working" +echo "" +echo "🔍 Useful Commands:" +echo " - Check pending requests:" +echo " flow scripts execute cadence/scripts/check_pending_requests.cdc --network testnet" +echo "" +echo " - Check handler status:" +echo " flow scripts execute cadence/scripts/check_tidemanager_status.cdc --network testnet" +echo "" diff --git a/flow.json b/flow.json new file mode 100644 index 0000000..d043ed9 --- /dev/null +++ b/flow.json @@ -0,0 +1,320 @@ +{ + "contracts": { + "FlowVaults": { + "source": "./lib/flow-vaults-sc/cadence/contracts/FlowVaults.cdc", + "aliases": { + "emulator": "045a1763c93006ca", + "testing": "0000000000000007", + "testnet": "3bda2f90274dbc9b" + } + }, + "FlowVaultsClosedBeta": { + "source": "./lib/flow-vaults-sc/cadence/contracts/FlowVaultsClosedBeta.cdc", + "aliases": { + "emulator": "045a1763c93006ca", + "testing": "0000000000000007", + "testnet": "3bda2f90274dbc9b" + } + }, + "FlowVaultsEVM": { + "source": "./cadence/contracts/FlowVaultsEVM.cdc", + "aliases": { + "emulator": "045a1763c93006ca", + "testing": "0000000000000007", + "testnet": "54069c7c195d9c3c" + } + }, + "FlowVaultsTransactionHandler": { + "source": "./cadence/contracts/FlowVaultsTransactionHandler.cdc", + "aliases": { + "emulator": "045a1763c93006ca", + "testing": "0000000000000007", + "testnet": "54069c7c195d9c3c" + } + } + }, + "dependencies": { + "BandOracle": { + "source": "mainnet://6801a6222ebf784a.BandOracle", + "hash": "be8c986f46eccfe55a25447e1b7fa07e95769ac4ca11918833130a4bf3297b16", + "aliases": { + "mainnet": "6801a6222ebf784a" + } + }, + "BandOracleConnectors": { + "source": "mainnet://f627b5c89141ed99.BandOracleConnectors", + "hash": "58721b4bd32126489a9d24a1bf229c8296457813f4c733d9e263d2caac070339", + "aliases": { + "mainnet": "f627b5c89141ed99" + } + }, + "Burner": { + "source": "mainnet://f233dcee88fe0abe.Burner", + "hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "CrossVMMetadataViews": { + "source": "mainnet://1d7e57aa55817448.CrossVMMetadataViews", + "hash": "dded0271279d3ca75f30b56f7552994d8b8bc4f75ef94a4a8d9d6b089e06c25c", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" + } + }, + "DeFiActions": { + "source": "mainnet://92195d814edf9cb0.DeFiActions", + "hash": "67175b2a2569bdff79c221ec7ac823c79dd59c83bce07582cfc3b675dfbe6207", + "aliases": { + "emulator": "045a1763c93006ca", + "mainnet": "92195d814edf9cb0", + "testing": "0000000000000007", + "testnet": "0b11b1848a8aa2c0" + } + }, + "DeFiActionsMathUtils": { + "source": "mainnet://92195d814edf9cb0.DeFiActionsMathUtils", + "hash": "f2ae511846ea9a545380968837f47a4198447c008e575047f3ace3b7cf782067", + "aliases": { + "emulator": "045a1763c93006ca", + "mainnet": "92195d814edf9cb0", + "testing": "0000000000000007", + "testnet": "0b11b1848a8aa2c0" + } + }, + "DeFiActionsUtils": { + "source": "mainnet://92195d814edf9cb0.DeFiActionsUtils", + "hash": "d01ab8cd8a57aee3e35ca8a9038f98de2321d6246e03285e0107ef6b4191bff1", + "aliases": { + "emulator": "045a1763c93006ca", + "mainnet": "92195d814edf9cb0", + "testing": "0000000000000007", + "testnet": "0b11b1848a8aa2c0" + } + }, + "EVM": { + "source": "mainnet://e467b9dd11fa00df.EVM", + "hash": "df2065d3eebc1e690e0b52a3f293bdf6c22780c7a9e7ef48a708a651b87abdf0", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "e467b9dd11fa00df", + "testnet": "8c5303eaa26202d6" + } + }, + "EVMNativeFLOWConnectors": { + "source": "mainnet://cc15a0c9c656b648.EVMNativeFLOWConnectors", + "hash": "3335dbd2f14b317f982ab8e0371bf7b089b66b53f24ed83cf1168c7a4c983eb3", + "aliases": { + "mainnet": "cc15a0c9c656b648" + } + }, + "FlowFees": { + "source": "mainnet://f919ee77447b7497.FlowFees", + "hash": "d02bc8295c0434cf2b0a96a77d992f49f52e7865debda84e7a21e176e163a680", + "aliases": { + "emulator": "e5a8b7f23e8b548f", + "mainnet": "f919ee77447b7497", + "testnet": "912d5440f7e3769e" + } + }, + "FlowStorageFees": { + "source": "mainnet://e467b9dd11fa00df.FlowStorageFees", + "hash": "e38d8a95f6518b8ff46ce57dfa37b4b850b3638f33d16333096bc625b6d9b51a", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "e467b9dd11fa00df", + "testnet": "8c5303eaa26202d6" + } + }, + "FlowToken": { + "source": "mainnet://1654653399040a61.FlowToken", + "hash": "cefb25fd19d9fc80ce02896267eb6157a6b0df7b1935caa8641421fe34c0e67a", + "aliases": { + "emulator": "0ae53cb6e3f42a79", + "mainnet": "1654653399040a61", + "testnet": "7e60df042a9c0868" + } + }, + "FlowTransactionScheduler": { + "source": "mainnet://e467b9dd11fa00df.FlowTransactionScheduler", + "hash": "312885f5fa3bc70327dfb59edc5da6d30b826002c322db8c566ddf17099310ac", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "e467b9dd11fa00df", + "testnet": "8c5303eaa26202d6" + } + }, + "FlowTransactionSchedulerUtils": { + "source": "mainnet://e467b9dd11fa00df.FlowTransactionSchedulerUtils", + "hash": "2e26d0bf8e6278b79880a47cb3cd55c499777fb96d76bde3f647b546805bc470", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "e467b9dd11fa00df", + "testnet": "8c5303eaa26202d6" + } + }, + "FungibleToken": { + "source": "mainnet://f233dcee88fe0abe.FungibleToken", + "hash": "23c1159cf99b2b039b6b868d782d57ae39b8d784045d81597f100a4782f0285b", + "aliases": { + "emulator": "ee82856bf20e2aa6", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "FungibleTokenConnectors": { + "source": "mainnet://1d9a619393e9fb53.FungibleTokenConnectors", + "hash": "01dd4a81d57f079316ff27e3980de1b895e2c50002e47d3c20f68bbf694e54b0", + "aliases": { + "emulator": "045a1763c93006ca", + "mainnet": "1d9a619393e9fb53", + "testing": "0000000000000007", + "testnet": "4cd02f8de4122c84" + } + }, + "FungibleTokenMetadataViews": { + "source": "mainnet://f233dcee88fe0abe.FungibleTokenMetadataViews", + "hash": "dff704a6e3da83997ed48bcd244aaa3eac0733156759a37c76a58ab08863016a", + "aliases": { + "emulator": "ee82856bf20e2aa6", + "mainnet": "f233dcee88fe0abe", + "testnet": "9a0766d93b6608b7" + } + }, + "IncrementFiStakingConnectors": { + "source": "mainnet://efa9bd7d1b17f1ed.IncrementFiStakingConnectors", + "hash": "ad0fafe9446d59018bcf2091fa11fa15ce34e7bb1f1ad660b3c7ce0068be6c6b", + "aliases": { + "mainnet": "efa9bd7d1b17f1ed" + } + }, + "MetadataViews": { + "source": "mainnet://1d7e57aa55817448.MetadataViews", + "hash": "9032f46909e729d26722cbfcee87265e4f81cd2912e936669c0e6b510d007e81", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" + } + }, + "NonFungibleToken": { + "source": "mainnet://1d7e57aa55817448.NonFungibleToken", + "hash": "b63f10e00d1a814492822652dac7c0574428a200e4c26cb3c832c4829e2778f0", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" + } + }, + "StableSwapFactory": { + "source": "mainnet://b063c16cac85dbd1.StableSwapFactory", + "hash": "46318aee6fd29616c8048c23210d4c4f5b172eb99a0ca911fbd849c831a52a0b", + "aliases": { + "mainnet": "b063c16cac85dbd1" + } + }, + "Staking": { + "source": "mainnet://1b77ba4b414de352.Staking", + "hash": "2faa0180c406f783165d6b2e3370076f126ac6a0e8b800ace55707e838147811", + "aliases": { + "mainnet": "1b77ba4b414de352" + } + }, + "StakingError": { + "source": "mainnet://1b77ba4b414de352.StakingError", + "hash": "f76be3a19f8b640149fa0860316a34058b96c4ac2154486e337fd449306c730e", + "aliases": { + "mainnet": "1b77ba4b414de352" + } + }, + "SwapConfig": { + "source": "mainnet://b78ef7afa52ff906.SwapConfig", + "hash": "ccafdb89804887e4e39a9b8fdff5c0ff0d0743505282f2a8ecf86c964e691c82", + "aliases": { + "mainnet": "b78ef7afa52ff906" + } + }, + "SwapConnectors": { + "source": "mainnet://0bce04a00aedf132.SwapConnectors", + "hash": "5cec3b1be3c454d3949686ddfb4cb5dc36e91ae7cec4cca31f3d416cd7772006", + "aliases": { + "emulator": "045a1763c93006ca", + "mainnet": "0bce04a00aedf132", + "testing": "0000000000000007", + "testnet": "ad228f1c13a97ec1" + } + }, + "SwapError": { + "source": "mainnet://b78ef7afa52ff906.SwapError", + "hash": "7d13a652a1308af387513e35c08b4f9a7389a927bddf08431687a846e4c67f21", + "aliases": { + "mainnet": "b78ef7afa52ff906" + } + }, + "SwapInterfaces": { + "source": "mainnet://b78ef7afa52ff906.SwapInterfaces", + "hash": "570bb4b9c8da8e0caa8f428494db80779fb906a66cc1904c39a2b9f78b89c6fa", + "aliases": { + "mainnet": "b78ef7afa52ff906" + } + }, + "ViewResolver": { + "source": "mainnet://1d7e57aa55817448.ViewResolver", + "hash": "374a1994046bac9f6228b4843cb32393ef40554df9bd9907a702d098a2987bde", + "aliases": { + "emulator": "f8d6e0586b0a20c7", + "mainnet": "1d7e57aa55817448", + "testnet": "631e88ae7f1d7c20" + } + } + }, + "networks": { + "emulator": "127.0.0.1:3569", + "mainnet": "access.mainnet.nodes.onflow.org:9000", + "testing": "127.0.0.1:3569", + "testnet": "access.devnet.nodes.onflow.org:9000" + }, + "accounts": { + "emulator-account": { + "address": "f8d6e0586b0a20c7", + "key": { + "type": "file", + "location": "lib/flow-vaults-sc/local/emulator-account.pkey" + } + }, + "testnet-account": { + "address": "54069c7c195d9c3c", + "key": { + "type": "hex", + "signatureAlgorithm": "ECDSA_secp256k1", + "hashAlgorithm": "SHA2_256", + "privateKey": "cdb902b028f34a4d90b893c67da4b489880af86efa2a99c436b577ab7781d16d" + } + }, + "tidal": { + "address": "045a1763c93006ca", + "key": { + "type": "file", + "location": "lib/flow-vaults-sc/local/emulator-tidal.pkey" + } + } + }, + "deployments": { + "emulator": { + "tidal": [ + "FlowVaultsTransactionHandler", + "FlowVaultsEVM" + ] + }, + "testnet": { + "testnet-account": [ + "FlowVaultsTransactionHandler", + "FlowVaultsEVM" + ] + } + } +} \ No newline at end of file diff --git a/lib/flow-vaults-sc b/lib/flow-vaults-sc new file mode 160000 index 0000000..2164bf4 --- /dev/null +++ b/lib/flow-vaults-sc @@ -0,0 +1 @@ +Subproject commit 2164bf43892f8149e74987e624b398c8433c1e6c diff --git a/local/deploy_full_stack.sh b/local/deploy_full_stack.sh new file mode 100755 index 0000000..12e242c --- /dev/null +++ b/local/deploy_full_stack.sh @@ -0,0 +1,160 @@ +#!/bin/bash + +set -e # Exit on any error + +# Configuration - Edit these values as needed +DEPLOYER_EOA="0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF" +DEPLOYER_FUNDING="50.46" + +USER_A_EOA="0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69" +USER_A_FUNDING="1234.12" + +RPC_URL="localhost:8545" + +# ============================================ +# VERIFY EVM GATEWAY IS READY +# ============================================ +echo "=== Verifying EVM Gateway is ready ===" + +MAX_GATEWAY_WAIT=30 +GATEWAY_COUNTER=0 +while [ $GATEWAY_COUNTER -lt $MAX_GATEWAY_WAIT ]; do + # Try to connect to EVM Gateway + GATEWAY_RESPONSE=$(curl -s -X POST http://$RPC_URL \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' || echo "") + + if echo "$GATEWAY_RESPONSE" | grep -q "0x"; then + echo "✓ EVM Gateway is ready and responding" + break + fi + + echo "Waiting for EVM Gateway to be ready... ($((GATEWAY_COUNTER + 1))/$MAX_GATEWAY_WAIT)" + sleep 2 + GATEWAY_COUNTER=$((GATEWAY_COUNTER + 1)) +done + +if [ $GATEWAY_COUNTER -ge $MAX_GATEWAY_WAIT ]; then + echo "❌ EVM Gateway is not ready after ${MAX_GATEWAY_WAIT} attempts" + echo "Last response: $GATEWAY_RESPONSE" + exit 1 +fi + +# Extra buffer to ensure full readiness +sleep 2 + +# ============================================ +# SETUP ACCOUNTS +# ============================================ +echo "=== Setting up accounts ===" + +# Fund deployer on EVM side +echo "Funding deployer account ($DEPLOYER_EOA) with $DEPLOYER_FUNDING FLOW..." +flow transactions send ./cadence/transactions/fund_evm_from_coa.cdc \ + "$DEPLOYER_EOA" "$DEPLOYER_FUNDING" + +# Fund userA on EVM side +echo "Funding userA account ($USER_A_EOA) with $USER_A_FUNDING FLOW..." +flow transactions send ./cadence/transactions/fund_evm_from_coa.cdc \ + "$USER_A_EOA" "$USER_A_FUNDING" + +echo "✓ Accounts setup complete" +echo "" + +# ============================================ +# DEPLOY CONTRACTS +# ============================================ +echo "=== Deploying contracts ===" + +# Wait for COA to be available with retry logic +MAX_COA_ATTEMPTS=10 +COA_ATTEMPT=0 +COA_ADDRESS="" + +while [ $COA_ATTEMPT -lt $MAX_COA_ATTEMPTS ]; do + COA_ADDRESS=$(flow scripts execute ./cadence/scripts/get_coa_address.cdc 045a1763c93006ca 2>/dev/null | grep "Result:" | cut -d'"' -f2 || echo "") + + if [ ! -z "$COA_ADDRESS" ]; then + break + fi + + COA_ATTEMPT=$((COA_ATTEMPT + 1)) + if [ $COA_ATTEMPT -lt $MAX_COA_ATTEMPTS ]; then + echo "Waiting for COA... ($COA_ATTEMPT/$MAX_COA_ATTEMPTS)" + sleep 2 + fi +done + +if [ -z "$COA_ADDRESS" ]; then + echo "❌ Failed to get COA address after $MAX_COA_ATTEMPTS attempts" + exit 1 +fi + +echo "COA Address: $COA_ADDRESS" + +# Export for Foundry +export COA_ADDRESS=$COA_ADDRESS + +# Verify EVM Gateway one more time before Solidity deployment +echo "Final EVM Gateway verification before deployment..." +FINAL_CHECK=$(curl -s -X POST http://$RPC_URL \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"net_version","params":[],"id":1}' || echo "") + +if ! echo "$FINAL_CHECK" | grep -q "result"; then + echo "❌ EVM Gateway not responding properly before deployment" + echo "Response: $FINAL_CHECK" + exit 1 +fi + +echo "✓ EVM Gateway confirmed ready for deployment" + +# Deploy FlowVaultsRequests Solidity contract +echo "Deploying FlowVaultsRequests contract to $RPC_URL..." +DEPLOYMENT_OUTPUT=$(forge script ./solidity/script/DeployFlowVaultsRequests.s.sol \ + --rpc-url "http://$RPC_URL" \ + --broadcast \ + --legacy \ + --optimize \ + --optimizer-runs 1000 \ + --via-ir 2>&1) + +echo "$DEPLOYMENT_OUTPUT" + +# Extract the deployed contract address from the output +FLOW_VAULTS_REQUESTS_CONTRACT=$(echo "$DEPLOYMENT_OUTPUT" | grep "FlowVaultsRequests deployed at:" | sed 's/.*: //') + +if [ -z "$FLOW_VAULTS_REQUESTS_CONTRACT" ]; then + echo "❌ Failed to extract FlowVaultsRequests contract address from deployment" + exit 1 +fi + +echo "✓ FlowVaultsRequests contract deployed at: $FLOW_VAULTS_REQUESTS_CONTRACT" +echo "" + +# ============================================ +# INITIALIZE PROJECT +# ============================================ +echo "=== Initializing project ===" + +# Deploy Cadence contracts (ignore failures for already-deployed contracts) +echo "Deploying Cadence contracts..." +flow project deploy || echo "⚠ Some contracts may already be deployed, continuing..." + +# Setup worker with beta badge +echo "Setting up worker with badge for contract $FLOW_VAULTS_REQUESTS_CONTRACT..." +flow transactions send ./cadence/transactions/setup_worker_with_badge.cdc \ + "$FLOW_VAULTS_REQUESTS_CONTRACT" \ + --signer tidal --gas-limit 9999 + +echo "✓ Project initialization complete" + +echo "" +echo "=========================================" +echo "✓ Full stack deployment complete!" +echo "=========================================" +echo "" +echo "FlowVaultsRequests Contract: $FLOW_VAULTS_REQUESTS_CONTRACT" +echo "" +echo "Export this for use in other scripts:" +echo "export FLOW_VAULTS_REQUESTS_CONTRACT=$FLOW_VAULTS_REQUESTS_CONTRACT" \ No newline at end of file diff --git a/local/run_transaction_handler.sh b/local/run_transaction_handler.sh new file mode 100755 index 0000000..e714c81 --- /dev/null +++ b/local/run_transaction_handler.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# Flow Vaults EVM Bridge - Scheduled Transaction Setup +# This script initializes the transaction handler and schedules the first execution + +set -e # Exit on any error + +echo "================================================" +echo "Flow Vaults EVM Bridge - Scheduled Txn Setup" +echo "================================================" +echo "" + +# Step 1: Initialize the Transaction Handler +echo "Step 1: Initializing Transaction Handler..." +flow transactions send ./cadence/transactions/init_flow_vaults_transaction_handler.cdc \ + --signer tidal --gas-limit 9999 + +echo "✅ Transaction Handler initialized" +echo "" + +# Step 2: Schedule Initial Execution +echo "Step 2: Scheduling initial execution..." +echo "Parameters:" +echo " - Delay: 3 seconds" +echo " - Priority: Medium (1)" +echo " - Execution Effort: 6000" +echo "" + +flow transactions send ./cadence/transactions/schedule_initial_flow_vaults_execution.cdc \ + --args-json '[ + {"type":"UFix64","value":"3.0"}, + {"type":"UInt8","value":"1"}, + {"type":"UInt64","value":"6000"} + ]' \ + --signer tidal --gas-limit 9999 + +echo "✅ Initial execution scheduled" +echo "" +echo "================================================" +echo "Setup Complete!" +echo "================================================" +echo "" +echo "The FlowVaultsEVM worker will process requests in 10 seconds." +echo "After that, it will need to be rescheduled manually or implement" +echo "self-scheduling logic." +echo "" \ No newline at end of file diff --git a/local/setup_and_run_emulator.sh b/local/setup_and_run_emulator.sh new file mode 100755 index 0000000..01fc51d --- /dev/null +++ b/local/setup_and_run_emulator.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +set -e # Exit on any error + +# install Flow Vaults submodule as dependency +git submodule update --init --recursive + +# ============================================ +# CLEANUP SECTION - All cleanup operations +# ============================================ +echo "Starting cleanup process..." + +# 1. Kill any existing processes on required ports +echo "Killing existing processes on ports..." +lsof -ti :8080 | xargs kill -9 2>/dev/null || true +lsof -ti :8545 | xargs kill -9 2>/dev/null || true +lsof -ti :3569 | xargs kill -9 2>/dev/null || true +lsof -ti :8888 | xargs kill -9 2>/dev/null || true + +# Brief pause to ensure ports are released +sleep 2 + +# 2. Clean the db directory (only if it exists) +echo "Cleaning ./db directory..." +if [ -d "./db" ]; then + rm -rf ./db/* + echo "Database directory cleaned." +else + echo "Database directory does not exist, skipping..." +fi + +# 3. Clean the imports directory +echo "Cleaning ./imports directory..." +if [ -d "./imports" ]; then + rm -rf ./imports/* + echo "Imports directory cleaned." +else + echo "Imports directory does not exist, creating it..." + mkdir -p ./imports +fi + +echo "Cleanup completed!" +echo "" +# ============================================ +# END CLEANUP SECTION +# ============================================ + +# Install dependencies - auto-answer yes to all prompts +echo "Installing Flow dependencies..." +flow deps install --skip-alias --skip-deployments + +# ============================================ +# FLOW-VAULTS-SC SETUP (using univ3_test pattern) +# ============================================ +echo "Setting up flow-vaults-sc environment..." +cd ./lib/flow-vaults-sc + +# Start Flow Emulator (runs in background) +./local/run_emulator.sh + +# Setup wallets (creates test accounts) +./local/setup_wallets.sh + +# Start EVM Gateway (runs in background) +./local/run_evm_gateway.sh + +echo "Setup PunchSwap" +./local/punchswap/setup_punchswap.sh +./local/punchswap/e2e_punchswap.sh + +echo "Setup emulator" +./local/setup_emulator.sh + +# Bridge tokens (MOET, USDC, WBTC) and setup liquidity pools +./local/setup_bridged_tokens.sh + +cd ../.. + +echo "" +echo "=========================================" +echo "✓ Flow Emulator & EVM Gateway are running" +echo "✓ FlowVaults with TracerStrategy configured" +echo "✓ Ready for FlowVaultsEVM deployment" +echo "=========================================" \ No newline at end of file diff --git a/local/test_tide_full_flow.sh b/local/test_tide_full_flow.sh new file mode 100755 index 0000000..3c2af78 --- /dev/null +++ b/local/test_tide_full_flow.sh @@ -0,0 +1,311 @@ +name: Tide Full Flow CI + +# This workflow tests the complete tide lifecycle: +# 1. Create tide with initial deposit (10 FLOW) +# 2. Add additional deposit (20 FLOW) - Total: 30 FLOW +# 3. Withdraw half (15 FLOW) - Remaining: 15 FLOW +# 4. Close tide (withdraw remaining 15 FLOW and close position) + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + integration-test: + name: End-to-End Tide Full Flow Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PAT }} + submodules: recursive + + - name: Install Required Tools + run: | + sudo apt-get update && sudo apt-get install -y lsof netcat-openbsd jq bc + + - name: Install Flow CLI + run: sh -ci "$(curl -fsSL https://raw.githubusercontent.com/onflow/flow-cli/master/install.sh)" + + - name: Update PATH + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Verify Flow CLI Installation + run: flow version + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Make scripts executable + run: | + chmod +x ./local/setup_and_run_emulator.sh + chmod +x ./local/deploy_full_stack.sh + + # Step 1: Setup environment and run emulator in background + - name: Setup and Run Emulator + run: | + ./local/setup_and_run_emulator.sh & + sleep 80 # Wait for emulator to fully start + + # Step 2: Deploy full stack + - name: Deploy Full Stack + run: | + DEPLOYMENT_OUTPUT=$(./local/deploy_full_stack.sh) + echo "$DEPLOYMENT_OUTPUT" + FLOW_VAULTS_REQUESTS_CONTRACT=$(echo "$DEPLOYMENT_OUTPUT" | grep "FlowVaultsRequests Contract:" | sed 's/.*: //') + echo "CONTRACT_ADDRESS=$FLOW_VAULTS_REQUESTS_CONTRACT" >> $GITHUB_ENV + echo "✅ Contract deployed at: $FLOW_VAULTS_REQUESTS_CONTRACT" + + # Step 3: Initial State Check + - name: Check Initial State + run: | + echo "=== Checking Initial State ===" + INITIAL_CHECK=$(flow scripts execute ./cadence/scripts/check_tide_details.cdc 0x045a1763c93006ca) + echo "$INITIAL_CHECK" + + # Verify no tides exist initially + INITIAL_TIDES=$(echo "$INITIAL_CHECK" | jq -r '.totalMappedTides // 0') + if [ "$INITIAL_TIDES" -eq 0 ]; then + echo "✅ Initial state confirmed: No tides exist" + else + echo "⚠️ Warning: Found $INITIAL_TIDES existing tides" + fi + echo "" + + # Step 4: Create tide from EVM (10 FLOW) + - name: Create Tide Request from EVM (10 FLOW) + run: | + echo "=== Creating Tide with 10 FLOW Initial Deposit ===" + forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations \ + --sig "runCreateTide(address)" ${{ env.CONTRACT_ADDRESS }} \ + --rpc-url http://localhost:8545 \ + --broadcast \ + --legacy + echo "✅ Create tide request sent" + + # Step 5: Process create tide request + - name: Process Create Tide Request + run: | + echo "Processing create tide request..." + flow transactions send ./cadence/transactions/process_requests.cdc --signer tidal --gas-limit 9999 + echo "✅ Create tide request processed" + + # Step 6: Verify tide creation + - name: Verify Tide Creation + run: | + echo "=== Verifying Tide Creation ===" + TIDE_CHECK=$(flow scripts execute ./cadence/scripts/check_tide_details.cdc 0x045a1763c93006ca) + echo "$TIDE_CHECK" + + # Check total tides increased + TOTAL_TIDES=$(echo "$TIDE_CHECK" | jq -r '.totalMappedTides // 0') + if [ "$TOTAL_TIDES" -eq 1 ]; then + echo "✅ Tide count verified: $TOTAL_TIDES tide(s) exist" + else + echo "❌ Expected 1 tide, found $TOTAL_TIDES" + exit 1 + fi + + # Check EVM address mapping + EVM_ADDRESS=$(echo "$TIDE_CHECK" | jq -r '.evmMappings[0].evmAddress // ""') + EXPECTED_EVM="6813eb9362372eef6200f3b1dbc3f819671cba69" + if [ "$EVM_ADDRESS" = "$EXPECTED_EVM" ]; then + echo "✅ EVM address mapping verified: $EVM_ADDRESS" + else + echo "❌ EVM address mismatch. Expected: $EXPECTED_EVM, Got: $EVM_ADDRESS" + exit 1 + fi + + # Extract and save tide ID + TIDE_ID=$(echo "$TIDE_CHECK" | jq -r '.evmMappings[0].tideIds[0] // 0') + echo "TIDE_ID=$TIDE_ID" >> $GITHUB_ENV + echo "✅ Tide created with ID: $TIDE_ID" + + # TODO: Add balance check when available in script + echo "ℹ️ Initial deposit: 10 FLOW (verification pending script support)" + echo "" + + # Step 7: Deposit to the created tide (add 20 FLOW) + - name: Deposit to Tide from EVM (20 FLOW) + run: | + echo "=== Depositing 20 FLOW to Tide ID: ${{ env.TIDE_ID }} ===" + forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations \ + --sig "runDepositToTide(address,uint64)" ${{ env.CONTRACT_ADDRESS }} ${{ env.TIDE_ID }} \ + --rpc-url http://localhost:8545 \ + --broadcast \ + --legacy + echo "✅ Deposit request sent" + + # Step 8: Process deposit request + - name: Process Deposit Request + run: | + echo "Processing deposit request..." + flow transactions send ./cadence/transactions/process_requests.cdc --signer tidal --gas-limit 9999 + echo "✅ Deposit request processed" + + # Step 9: Verify deposit + - name: Verify Deposit + run: | + echo "=== Verifying Deposit ===" + TIDE_CHECK=$(flow scripts execute ./cadence/scripts/check_tide_details.cdc 0x045a1763c93006ca) + echo "$TIDE_CHECK" + + # Verify tide still exists + TOTAL_TIDES=$(echo "$TIDE_CHECK" | jq -r '.totalMappedTides // 0') + if [ "$TOTAL_TIDES" -eq 1 ]; then + echo "✅ Tide still active after deposit" + else + echo "❌ Tide count changed unexpectedly: $TOTAL_TIDES" + exit 1 + fi + + # Verify tide ID is still in mapping + TIDE_EXISTS=$(echo "$TIDE_CHECK" | jq --arg tid "${{ env.TIDE_ID }}" '.evmMappings[0].tideIds | contains([($tid | tonumber)])') + if [ "$TIDE_EXISTS" = "true" ]; then + echo "✅ Tide ID ${{ env.TIDE_ID }} still mapped correctly" + else + echo "❌ Tide ID ${{ env.TIDE_ID }} not found in mapping" + exit 1 + fi + + echo "ℹ️ Expected balance after deposit: 30 FLOW (10 initial + 20 deposit)" + echo "" + + # Step 10: Withdraw half from tide (withdraw 15 FLOW) + - name: Withdraw from Tide (15 FLOW) + run: | + echo "=== Withdrawing 15 FLOW from Tide ID: ${{ env.TIDE_ID }} ===" + forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations \ + --sig "runWithdrawFromTide(address,uint64,uint256)" \ + ${{ env.CONTRACT_ADDRESS }} \ + ${{ env.TIDE_ID }} \ + 15000000000000000000 \ + --rpc-url http://localhost:8545 \ + --broadcast \ + --legacy + echo "✅ Withdraw request sent" + + # Step 11: Process withdraw request + - name: Process Withdraw Request + run: | + echo "Processing withdraw request..." + flow transactions send ./cadence/transactions/process_requests.cdc --signer tidal --gas-limit 9999 + echo "✅ Withdraw request processed" + + # Step 12: Verify withdrawal + - name: Verify Withdrawal + run: | + echo "=== Verifying Withdrawal ===" + TIDE_CHECK=$(flow scripts execute ./cadence/scripts/check_tide_details.cdc 0x045a1763c93006ca) + echo "$TIDE_CHECK" + + # Verify tide still exists (shouldn't be closed after partial withdrawal) + TOTAL_TIDES=$(echo "$TIDE_CHECK" | jq -r '.totalMappedTides // 0') + if [ "$TOTAL_TIDES" -eq 1 ]; then + echo "✅ Tide still active after partial withdrawal" + else + echo "❌ Tide count changed unexpectedly: $TOTAL_TIDES" + exit 1 + fi + + # Verify tide ID is still in mapping + TIDE_EXISTS=$(echo "$TIDE_CHECK" | jq --arg tid "${{ env.TIDE_ID }}" '.evmMappings[0].tideIds | contains([($tid | tonumber)])') + if [ "$TIDE_EXISTS" = "true" ]; then + echo "✅ Tide ID ${{ env.TIDE_ID }} still active" + else + echo "❌ Tide ID ${{ env.TIDE_ID }} unexpectedly removed" + exit 1 + fi + + echo "ℹ️ Expected balance after withdrawal: 15 FLOW (30 - 15 withdrawn)" + echo "" + + # Step 13: Close tide (withdraws remaining funds and closes position) + - name: Close Tide + run: | + echo "=== Closing Tide ID: ${{ env.TIDE_ID }} ===" + forge script ./solidity/script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations \ + --sig "runCloseTide(address,uint64)" \ + ${{ env.CONTRACT_ADDRESS }} \ + ${{ env.TIDE_ID }} \ + --rpc-url http://localhost:8545 \ + --broadcast \ + --legacy + echo "✅ Close tide request sent" + + # Step 14: Process close tide request + - name: Process Close Tide Request + run: | + echo "Processing close tide request..." + flow transactions send ./cadence/transactions/process_requests.cdc --signer tidal --gas-limit 9999 + echo "✅ Close tide request processed" + + # Step 15: Verify tide was closed + - name: Verify Tide Closure + run: | + echo "=== Verifying Tide Closure ===" + TIDE_CHECK=$(flow scripts execute ./cadence/scripts/check_tide_details.cdc 0x045a1763c93006ca) + echo "$TIDE_CHECK" + + # Check if tide count decreased + TOTAL_TIDES=$(echo "$TIDE_CHECK" | jq -r '.totalMappedTides // 0') + if [ "$TOTAL_TIDES" -eq 0 ]; then + echo "✅ All tides closed successfully" + else + # Check if tide ID is no longer in the mapping + TIDE_EXISTS=$(echo "$TIDE_CHECK" | jq --arg tid "${{ env.TIDE_ID }}" ' + .evmMappings[0].tideIds // [] | contains([($tid | tonumber)]) + ' 2>/dev/null || echo "false") + + if [ "$TIDE_EXISTS" = "false" ]; then + echo "✅ Tide ID ${{ env.TIDE_ID }} successfully removed from mapping" + else + echo "⚠️ Warning: Tide ID ${{ env.TIDE_ID }} may still be in mapping" + echo " Total tides remaining: $TOTAL_TIDES" + fi + fi + + # Check EVM address mapping + EVM_COUNT=$(echo "$TIDE_CHECK" | jq -r '.totalEVMAddresses // 0') + if [ "$EVM_COUNT" -eq 0 ]; then + echo "✅ EVM address mapping cleaned up" + else + TIDE_COUNT=$(echo "$TIDE_CHECK" | jq -r '.evmMappings[0].tideCount // 0' 2>/dev/null || echo "0") + echo "ℹ️ EVM address still registered with $TIDE_COUNT tide(s)" + fi + + echo "" + echo "=========================================" + echo "✅ TIDE FULL LIFECYCLE TEST COMPLETED!" + echo "=========================================" + echo "Summary:" + echo " 1. ✅ Created tide with 10 FLOW" + echo " 2. ✅ Deposited additional 20 FLOW (total: 30)" + echo " 3. ✅ Withdrew 15 FLOW (remaining: 15)" + echo " 4. ✅ Closed tide (withdrew final 15 FLOW)" + echo "=========================================" + + # Step 16: Final State Verification + - name: Final State Verification + run: | + echo "=== Final State Verification ===" + FINAL_CHECK=$(flow scripts execute ./cadence/scripts/check_tide_details.cdc 0x045a1763c93006ca) + + FINAL_TIDES=$(echo "$FINAL_CHECK" | jq -r '.totalMappedTides // 0') + FINAL_EVM_ADDRESSES=$(echo "$FINAL_CHECK" | jq -r '.totalEVMAddresses // 0') + + echo "Final state:" + echo " - Total active tides: $FINAL_TIDES" + echo " - Total EVM addresses with tides: $FINAL_EVM_ADDRESSES" + + if [ "$FINAL_TIDES" -eq 0 ]; then + echo "✅ System returned to clean state" + else + echo "ℹ️ System has $FINAL_TIDES active tide(s)" + fi \ No newline at end of file diff --git a/solidity/.github/workflows/test.yml b/solidity/.github/workflows/test.yml new file mode 100644 index 0000000..4481ec6 --- /dev/null +++ b/solidity/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/solidity/.gitignore b/solidity/.gitignore new file mode 100644 index 0000000..c871a7d --- /dev/null +++ b/solidity/.gitignore @@ -0,0 +1,15 @@ +# Forge lib +lib/ + +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +broadcast/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/solidity/README.md b/solidity/README.md new file mode 100644 index 0000000..8817d6a --- /dev/null +++ b/solidity/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/solidity/foundry.lock b/solidity/foundry.lock new file mode 100644 index 0000000..18da94f --- /dev/null +++ b/solidity/foundry.lock @@ -0,0 +1,11 @@ +{ + "../lib/flow-vaults-sc": { + "rev": "e2cc62f75907abf7a9aee667edd7fca4aa77ccf7" + }, + "lib/forge-std": { + "tag": { + "name": "v1.11.0", + "rev": "8e40513d678f392f398620b3ef2b418648b33e89" + } + } +} \ No newline at end of file diff --git a/solidity/foundry.toml b/solidity/foundry.toml new file mode 100644 index 0000000..209925b --- /dev/null +++ b/solidity/foundry.toml @@ -0,0 +1,7 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +fs_permissions = [{ access = "read", path = "../env" }] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/solidity/lib/forge-std b/solidity/lib/forge-std new file mode 160000 index 0000000..100b0d7 --- /dev/null +++ b/solidity/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 100b0d756adda67bc70aab816fa5a1a95dcf78b6 diff --git a/solidity/script/DeployFlowVaultsRequests.s.sol b/solidity/script/DeployFlowVaultsRequests.s.sol new file mode 100644 index 0000000..ba28415 --- /dev/null +++ b/solidity/script/DeployFlowVaultsRequests.s.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import "forge-std/Script.sol"; +import "../src/FlowVaultsRequests.sol"; + +contract DeployFlowVaultsRequests is Script { + function run() external returns (FlowVaultsRequests) { + uint256 deployerPrivateKey = vm.envOr( + "DEPLOYER_PRIVATE_KEY", + uint256(0x2) + ); + + address deployer = vm.addr(deployerPrivateKey); + console.log("Deployer address:", deployer); + console.log("Deployer balance:", deployer.balance); + + // Read COA address from environment variable + address coa = vm.envAddress("COA_ADDRESS"); + console.log("Using COA address:", coa); + + // Start broadcast with private key + vm.startBroadcast(deployerPrivateKey); + + FlowVaultsRequests flowVaultsRequests = new FlowVaultsRequests(coa); + + console.log( + "FlowVaultsRequests deployed at:", + address(flowVaultsRequests) + ); + console.log("NATIVE_FLOW constant:", flowVaultsRequests.NATIVE_FLOW()); + + vm.stopBroadcast(); + + return flowVaultsRequests; + } +} diff --git a/solidity/script/FlowVaultsTideOperations.s.sol b/solidity/script/FlowVaultsTideOperations.s.sol new file mode 100644 index 0000000..d7582e3 --- /dev/null +++ b/solidity/script/FlowVaultsTideOperations.s.sol @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import "forge-std/Script.sol"; +import "../src/FlowVaultsRequests.sol"; + +/** + * @title FlowVaultsTideOperations + * @notice Unified script for all Flow Vaults Tide operations on EVM side + * @dev Supports: CREATE_TIDE, DEPOSIT_TO_TIDE, WITHDRAW_FROM_TIDE, CLOSE_TIDE + * + * Usage: + * - CREATE_TIDE: forge script script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations --sig "runCreateTide(address)" --broadcast + * - DEPOSIT_TO_TIDE: forge script script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations --sig "runDepositToTide(address,uint64)" --broadcast + * - WITHDRAW_FROM_TIDE: forge script script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations --sig "runWithdrawFromTide(address,uint64,uint256)" --broadcast + * - CLOSE_TIDE: forge script script/FlowVaultsTideOperations.s.sol:FlowVaultsTideOperations --sig "runCloseTide(address,uint64)" --broadcast + * + * Environment Variables (optional): + * - USER_PRIVATE_KEY: Private key for signing (defaults to test key 0x3) + * - AMOUNT: Amount in wei for create/deposit operations (defaults to 10 ether) + */ +contract FlowVaultsTideOperations is Script { + // ============================================ + // Configuration + // ============================================ + + // NATIVE_FLOW constant (must match FlowVaultsRequests.sol) + address constant NATIVE_FLOW = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; + + // Default amount for operations (can be overridden via env var) + uint256 constant DEFAULT_AMOUNT = 10 ether; + + // Vault and strategy identifiers for testnet + // string constant VAULT_IDENTIFIER = "A.7e60df042a9c0868.FlowToken.Vault"; + // string constant STRATEGY_IDENTIFIER = + // "A.3bda2f90274dbc9b.FlowVaultsStrategies.TracerStrategy"; + + // Vault and strategy identifiers for emulator - CI testing + string constant VAULT_IDENTIFIER = "A.0ae53cb6e3f42a79.FlowToken.Vault"; + string constant STRATEGY_IDENTIFIER = + "A.045a1763c93006ca.FlowVaultsStrategies.TracerStrategy"; + + // ============================================ + // Public Entry Points + // ============================================ + + /// @notice Create a new Tide with default or ENV-specified amount + /// @param contractAddress The FlowVaultsRequests contract address + function runCreateTide(address contractAddress) public { + uint256 userPrivateKey = vm.envOr("USER_PRIVATE_KEY", uint256(0x3)); + uint256 amount = vm.envOr("AMOUNT", DEFAULT_AMOUNT); + address user = vm.addr(userPrivateKey); + + FlowVaultsRequests flowVaultsRequests = FlowVaultsRequests( + payable(contractAddress) + ); + + createTide(flowVaultsRequests, user, userPrivateKey, amount); + } + + /// @notice Deposit to an existing Tide with default or ENV-specified amount + /// @param contractAddress The FlowVaultsRequests contract address + /// @param tideId The Tide ID to deposit to + function runDepositToTide(address contractAddress, uint64 tideId) public { + uint256 userPrivateKey = vm.envOr("USER_PRIVATE_KEY", uint256(0x3)); + uint256 amount = vm.envOr("AMOUNT", DEFAULT_AMOUNT); + address user = vm.addr(userPrivateKey); + + FlowVaultsRequests flowVaultsRequests = FlowVaultsRequests( + payable(contractAddress) + ); + + depositToTide(flowVaultsRequests, user, userPrivateKey, tideId, amount); + } + + /// @notice Withdraw from a Tide + /// @param contractAddress The FlowVaultsRequests contract address + /// @param tideId The Tide ID to withdraw from + /// @param amount Amount to withdraw in wei + function runWithdrawFromTide( + address contractAddress, + uint64 tideId, + uint256 amount + ) public { + uint256 userPrivateKey = vm.envOr("USER_PRIVATE_KEY", uint256(0x3)); + address user = vm.addr(userPrivateKey); + + FlowVaultsRequests flowVaultsRequests = FlowVaultsRequests( + payable(contractAddress) + ); + + withdrawFromTide( + flowVaultsRequests, + user, + userPrivateKey, + tideId, + amount + ); + } + + /// @notice Close a Tide and withdraw all funds + /// @param contractAddress The FlowVaultsRequests contract address + /// @param tideId The Tide ID to close + function runCloseTide(address contractAddress, uint64 tideId) public { + uint256 userPrivateKey = vm.envOr("USER_PRIVATE_KEY", uint256(0x3)); + address user = vm.addr(userPrivateKey); + + FlowVaultsRequests flowVaultsRequests = FlowVaultsRequests( + payable(contractAddress) + ); + + closeTide(flowVaultsRequests, user, userPrivateKey, tideId); + } + + // ============================================ + // Internal Implementation Functions + // ============================================ + + function createTide( + FlowVaultsRequests flowVaultsRequests, + address user, + uint256 userPrivateKey, + uint256 amount + ) internal { + console.log("\n=== Creating New Tide ==="); + console.log("Amount:", amount); + console.log("Vault:", VAULT_IDENTIFIER); + console.log("Strategy:", STRATEGY_IDENTIFIER); + + require(user.balance >= amount, "Insufficient balance"); + + vm.startBroadcast(userPrivateKey); + + uint256 requestId = flowVaultsRequests.createTide{value: amount}( + NATIVE_FLOW, + amount, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + vm.stopBroadcast(); + + displayRequestDetails(flowVaultsRequests, requestId, user); + + console.log("\n=== Next Steps ==="); + console.log("1. Note the Request ID:", requestId); + console.log("2. Run Cadence worker to process this request"); + console.log("3. Tide ID will be assigned after processing"); + } + + // ============================================ + // Operation: DEPOSIT_TO_TIDE + // ============================================ + + function depositToTide( + FlowVaultsRequests flowVaultsRequests, + address user, + uint256 userPrivateKey, + uint64 tideId, + uint256 amount + ) internal { + console.log("\n=== Depositing to Existing Tide ==="); + console.log("Tide ID:", tideId); + console.log("Amount:", amount); + + require(user.balance >= amount, "Insufficient balance"); + + vm.startBroadcast(userPrivateKey); + + uint256 requestId = flowVaultsRequests.depositToTide{value: amount}( + tideId, + NATIVE_FLOW, + amount + ); + + vm.stopBroadcast(); + + displayRequestDetails(flowVaultsRequests, requestId, user); + + console.log("\n=== Next Steps ==="); + console.log("1. Note the Request ID:", requestId); + console.log("2. Run Cadence worker to process this deposit"); + } + + // ============================================ + // Operation: WITHDRAW_FROM_TIDE + // ============================================ + + function withdrawFromTide( + FlowVaultsRequests flowVaultsRequests, + address user, + uint256 userPrivateKey, + uint64 tideId, + uint256 amount + ) internal { + console.log("\n=== Withdrawing from Tide ==="); + console.log("Tide ID:", tideId); + console.log("Amount:", amount); + + vm.startBroadcast(userPrivateKey); + + uint256 requestId = flowVaultsRequests.withdrawFromTide(tideId, amount); + + vm.stopBroadcast(); + + displayRequestDetails(flowVaultsRequests, requestId, user); + + console.log("\n=== Next Steps ==="); + console.log("1. Note the Request ID:", requestId); + console.log("2. Run Cadence worker to process this withdrawal"); + console.log("3. Funds will be returned to your EVM address"); + } + + // ============================================ + // Operation: CLOSE_TIDE + // ============================================ + + function closeTide( + FlowVaultsRequests flowVaultsRequests, + address user, + uint256 userPrivateKey, + uint64 tideId + ) internal { + console.log("\n=== Closing Tide ==="); + console.log("Tide ID:", tideId); + + vm.startBroadcast(userPrivateKey); + + uint256 requestId = flowVaultsRequests.closeTide(tideId); + + vm.stopBroadcast(); + + displayRequestDetails(flowVaultsRequests, requestId, user); + + console.log("\n=== Next Steps ==="); + console.log("1. Note the Request ID:", requestId); + console.log("2. Run Cadence worker to process this closure"); + console.log("3. All funds will be returned to your EVM address"); + } + + // ============================================ + // Helper Functions + // ============================================ + + function displayRequestDetails( + FlowVaultsRequests flowVaultsRequests, + uint256 requestId, + address user + ) internal view { + FlowVaultsRequests.Request memory request = flowVaultsRequests + .getRequest(requestId); + + console.log("\n=== Request Created ==="); + console.log("Request ID:", request.id); + console.log("User:", request.user); + console.log("Type:", uint256(request.requestType)); + console.log("Status:", uint256(request.status)); + console.log("Token:", request.tokenAddress); + console.log("Amount:", request.amount); + console.log("Tide ID:", request.tideId); + console.log("Timestamp:", request.timestamp); + + if (bytes(request.vaultIdentifier).length > 0) { + console.log("Vault:", request.vaultIdentifier); + } + if (bytes(request.strategyIdentifier).length > 0) { + console.log("Strategy:", request.strategyIdentifier); + } + + uint256[] memory pendingIds = flowVaultsRequests.getPendingRequestIds(); + console.log("\n=== Queue Status ==="); + console.log("Total pending requests:", pendingIds.length); + + uint256 userBalance = flowVaultsRequests.getUserBalance( + user, + NATIVE_FLOW + ); + console.log("Your pending balance:", userBalance); + console.log("Your wallet balance:", user.balance); + } +} diff --git a/solidity/script/deploy_and_verify.sh b/solidity/script/deploy_and_verify.sh new file mode 100755 index 0000000..bdf25bb --- /dev/null +++ b/solidity/script/deploy_and_verify.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# Deploy and verify FlowVaultsRequests contract +# Run this script from the solidity/ directory + +# Load environment variables from parent .env file +export $(grep -v '^#' ../.env | xargs) + +echo "🚀 Deploying FlowVaultsRequests..." + +# Deploy the contract +forge script script/DeployFlowVaultsRequests.s.sol:DeployFlowVaultsRequests \ + --rpc-url https://testnet.evm.nodes.onflow.org \ + --broadcast \ + -vvvv + +# Extract the deployed contract address from the broadcast file +DEPLOYED_ADDRESS=$(jq -r '.transactions[0].contractAddress' broadcast/DeployFlowVaultsRequests.s.sol/545/run-latest.json) + +echo "" +echo "📝 Deployed contract address: $DEPLOYED_ADDRESS" +echo "" + +# Read COA address from .env file in parent directory +COA_ADDRESS=$(grep COA_ADDRESS ../.env | cut -d '=' -f2) + +echo "⏳ Waiting 60 seconds for block explorer to index the deployment..." +sleep 60 + +echo "🔍 Verifying contract..." +echo "COA Address (constructor arg): $COA_ADDRESS" +echo "" + +# Verify the contract +forge verify-contract \ + --rpc-url https://testnet.evm.nodes.onflow.org/ \ + --verifier blockscout \ + --verifier-url 'https://evm-testnet.flowscan.io/api/' \ + --constructor-args $(cast abi-encode "constructor(address)" $COA_ADDRESS) \ + --compiler-version 0.8.18 \ + $DEPLOYED_ADDRESS \ + src/FlowVaultsRequests.sol:FlowVaultsRequests + +echo "" +echo "✅ Deployment and verification complete!" +echo "Contract address: $DEPLOYED_ADDRESS" diff --git a/solidity/src/FlowVaultsRequests.sol b/solidity/src/FlowVaultsRequests.sol new file mode 100644 index 0000000..0c4cf57 --- /dev/null +++ b/solidity/src/FlowVaultsRequests.sol @@ -0,0 +1,863 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +/** + * @title FlowVaultsRequests + * @notice Request queue and fund escrow for EVM users to interact with Flow Vaults Cadence protocol + * @dev This contract holds user funds in escrow until processed by FlowVaultsEVM + */ +contract FlowVaultsRequests { + // ============================================ + // Custom Errors + // ============================================ + + error NotAuthorizedCOA(); + error NotOwner(); + error NotInAllowlist(); + error InvalidCOAAddress(); + error EmptyAddressArray(); + error CannotAllowlistZeroAddress(); + error AmountMustBeGreaterThanZero(); + error MsgValueMustEqualAmount(); + error MsgValueMustBeZero(); + error ERC20NotSupported(); + error RequestNotFound(); + error NotRequestOwner(); + error CanOnlyCancelPending(); + error RequestAlreadyFinalized(); + error InsufficientBalance(); + error TransferFailed(); + error BelowMinimumBalance(); + error TooManyPendingRequests(); + error InvalidTideId(); + + // ============================================ + // Constants + // ============================================ + + /// @notice Special address representing native $FLOW (similar to 1inch approach) + /// @dev Using recognizable pattern instead of address(0) for clarity + address public constant NATIVE_FLOW = + 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; + + // ============================================ + // Enums + // ============================================ + + enum RequestType { + CREATE_TIDE, + DEPOSIT_TO_TIDE, + WITHDRAW_FROM_TIDE, + CLOSE_TIDE + } + + enum RequestStatus { + PENDING, + COMPLETED, + FAILED + } + + // ============================================ + // Structs + // ============================================ + + struct Request { + uint256 id; + address user; + RequestType requestType; + RequestStatus status; + address tokenAddress; + uint256 amount; + uint64 tideId; // Only used for DEPOSIT/WITHDRAW/CLOSE + uint256 timestamp; + string message; // Error message or status details + string vaultIdentifier; // Cadence vault type identifier (e.g., "A.7e60df042a9c0868.FlowToken.Vault") + string strategyIdentifier; // Cadence strategy type identifier (e.g., "A.3bda2f90274dbc9b.FlowVaultsStrategies.TracerStrategy") + } + + // ============================================ + // State Variables + // ============================================ + + /// @notice Auto-incrementing request ID counter + uint256 private _requestIdCounter; + + /// @notice Authorized COA address (controlled by FlowVaultsEVM) + address public authorizedCOA; + + /// @notice Owner of the contract (for admin functions) + address public owner; + + /// @notice Allow list enabled flag + bool public allowlistEnabled; + + /// @notice Allow-listed addresses mapping + mapping(address => bool) public allowlisted; + + /// @notice Minimum balance required for deposits (can be updated by owner) + uint256 public minimumBalance; + + /// @notice Maximum pending requests allowed per user + uint256 public maxPendingRequestsPerUser; + + /// @notice Track pending request count per user + mapping(address => uint256) public userPendingRequestCount; + + /// @notice Registry of valid tide IDs created on Cadence side + mapping(uint64 => bool) public validTideIds; + + /// @notice Tide ownership mapping: tideId => owner address + mapping(uint64 => address) public tideOwners; + + /// @notice Pending user balances: user address => token address => balance + /// @dev These are funds in escrow waiting to be converted to Tides + mapping(address => mapping(address => uint256)) public pendingUserBalances; + + /// @notice Pending requests for efficient worker processing + mapping(uint256 => Request) public pendingRequests; + uint256[] public pendingRequestIds; + + // ============================================ + // Events + // ============================================ + + event RequestCreated( + uint256 indexed requestId, + address indexed user, + RequestType requestType, + address indexed tokenAddress, + uint256 amount, + uint64 tideId + ); + + event RequestProcessed( + uint256 indexed requestId, + RequestStatus status, + uint64 tideId, + string message + ); + + event RequestCancelled( + uint256 indexed requestId, + address indexed user, + uint256 refundAmount + ); + + event BalanceUpdated( + address indexed user, + address indexed tokenAddress, + uint256 newBalance + ); + + event FundsWithdrawn( + address indexed to, + address indexed tokenAddress, + uint256 amount + ); + + event AuthorizedCOAUpdated(address indexed oldCOA, address indexed newCOA); + + event AllowlistEnabled(bool enabled); + + event AddressesAddedToAllowlist(address[] addresses); + + event AddressesRemovedFromAllowlist(address[] addresses); + + event MinimumBalanceUpdated(uint256 oldMinimum, uint256 newMinimum); + + event MaxPendingRequestsPerUserUpdated(uint256 oldMax, uint256 newMax); + + event TideIdRegistered(uint64 indexed tideId); + + event RequestsDropped(uint256[] requestIds, address indexed droppedBy); + + // ============================================ + // Modifiers + // ============================================ + + modifier onlyAuthorizedCOA() { + _checkAuthorizedCOA(); + _; + } + + modifier onlyOwner() { + _checkOwner(); + _; + } + + modifier onlyAllowlisted() { + _checkAllowlisted(); + _; + } + + function _checkAuthorizedCOA() internal view { + if (msg.sender != authorizedCOA) revert NotAuthorizedCOA(); + } + + function _checkOwner() internal view { + if (msg.sender != owner) revert NotOwner(); + } + + function _checkAllowlisted() internal view { + if (allowlistEnabled && !allowlisted[msg.sender]) + revert NotInAllowlist(); + } + + // ============================================ + // Constructor + // ============================================ + + constructor(address coaAddress) { + owner = msg.sender; + authorizedCOA = coaAddress; + + _requestIdCounter = 1; + minimumBalance = 1000000000000000; + maxPendingRequestsPerUser = 100; + } + + // ============================================ + // Admin Functions + // ============================================ + + /// @notice Set the authorized COA address (can only be called by owner) + /// @param _coa The COA address controlled by FlowVaultsEVM + function setAuthorizedCOA(address _coa) external onlyOwner { + if (_coa == address(0)) revert InvalidCOAAddress(); + address oldCOA = authorizedCOA; + authorizedCOA = _coa; + emit AuthorizedCOAUpdated(oldCOA, _coa); + } + + /// @notice Enable or disable allow list enforcement + /// @param _enabled True to enable allow list, false to disable + function setAllowlistEnabled(bool _enabled) external onlyOwner { + allowlistEnabled = _enabled; + emit AllowlistEnabled(_enabled); + } + + /// @notice Add multiple addresses to allow list + /// @param _addresses Array of addresses to allow list + function batchAddToAllowlist( + address[] calldata _addresses + ) external onlyOwner { + if (_addresses.length == 0) revert EmptyAddressArray(); + + for (uint256 i = 0; i < _addresses.length; ) { + if (_addresses[i] == address(0)) + revert CannotAllowlistZeroAddress(); + allowlisted[_addresses[i]] = true; + unchecked { + ++i; + } + } + + emit AddressesAddedToAllowlist(_addresses); + } + + /// @notice Remove multiple addresses from allow list + /// @param _addresses Array of addresses to remove from allow list + function batchRemoveFromAllowlist( + address[] calldata _addresses + ) external onlyOwner { + if (_addresses.length == 0) revert EmptyAddressArray(); + + for (uint256 i = 0; i < _addresses.length; ) { + allowlisted[_addresses[i]] = false; + unchecked { + ++i; + } + } + + emit AddressesRemovedFromAllowlist(_addresses); + } + + /// @notice Set minimum balance required for deposits + /// @param _minimumBalance New minimum balance (in wei) + function setMinimumBalance(uint256 _minimumBalance) external onlyOwner { + uint256 oldMinimum = minimumBalance; + minimumBalance = _minimumBalance; + emit MinimumBalanceUpdated(oldMinimum, _minimumBalance); + } + + /// @notice Set maximum pending requests allowed per user + /// @param _maxRequests New maximum (0 = no limit) + function setMaxPendingRequestsPerUser( + uint256 _maxRequests + ) external onlyOwner { + uint256 oldMax = maxPendingRequestsPerUser; + maxPendingRequestsPerUser = _maxRequests; + emit MaxPendingRequestsPerUserUpdated(oldMax, _maxRequests); + } + + /// @notice Drop invalid/spam requests (admin function to clear backlog) + /// @param requestIds Array of request IDs to drop + function dropRequests(uint256[] calldata requestIds) external onlyOwner { + for (uint256 i = 0; i < requestIds.length; ) { + uint256 requestId = requestIds[i]; + Request storage request = pendingRequests[requestId]; + + if ( + request.id == requestId && + request.status == RequestStatus.PENDING + ) { + // Mark as failed + request.status = RequestStatus.FAILED; + request.message = "Dropped"; + + // Refund if necessary + if ( + (request.requestType == RequestType.CREATE_TIDE || + request.requestType == RequestType.DEPOSIT_TO_TIDE) && + request.amount > 0 + ) { + // Decrease pending balance + pendingUserBalances[request.user][ + request.tokenAddress + ] -= request.amount; + emit BalanceUpdated( + request.user, + request.tokenAddress, + pendingUserBalances[request.user][request.tokenAddress] + ); + + // Refund the funds + if (isNativeFlow(request.tokenAddress)) { + (bool success, ) = request.user.call{ + value: request.amount + }(""); + if (!success) revert TransferFailed(); + } + + emit FundsWithdrawn( + request.user, + request.tokenAddress, + request.amount + ); + } + + // Decrement user pending request count + if (userPendingRequestCount[request.user] > 0) { + userPendingRequestCount[request.user]--; + } + + // Remove from pending queue + _removePendingRequest(requestId); + + emit RequestProcessed( + requestId, + RequestStatus.FAILED, + request.tideId, + "Dropped" + ); + } + + unchecked { + ++i; + } + } + + emit RequestsDropped(requestIds, msg.sender); + } + + // ============================================ + // User Functions + // ============================================ + + /// @notice Create a new Tide (deposit funds to create position) + /// @param tokenAddress Address of token (use NATIVE_FLOW for native $FLOW) + /// @param amount Amount to deposit + /// @param vaultIdentifier Cadence vault type identifier (e.g., "A.7e60df042a9c0868.FlowToken.Vault") + /// @param strategyIdentifier Cadence strategy type identifier (e.g., "A.3bda2f90274dbc9b.FlowVaultsStrategies.TracerStrategy") + function createTide( + address tokenAddress, + uint256 amount, + string calldata vaultIdentifier, + string calldata strategyIdentifier + ) external payable onlyAllowlisted returns (uint256) { + _validateDeposit(tokenAddress, amount); + _checkPendingRequestLimit(); + + uint256 requestId = createRequest( + RequestType.CREATE_TIDE, + tokenAddress, + amount, + 0, // No tideId yet + vaultIdentifier, + strategyIdentifier + ); + + return requestId; + } + + /// @notice Deposit additional funds to existing Tide + /// @param tideId The Tide ID to deposit to + /// @param tokenAddress Address of token (use NATIVE_FLOW for native $FLOW) + /// @param amount Amount to deposit + function depositToTide( + uint64 tideId, + address tokenAddress, + uint256 amount + ) external payable onlyAllowlisted returns (uint256) { + _validateDeposit(tokenAddress, amount); + _validateTideId(tideId, msg.sender); + _checkPendingRequestLimit(); + + uint256 requestId = createRequest( + RequestType.DEPOSIT_TO_TIDE, + tokenAddress, + amount, + tideId, + "", // No vault identifier needed for deposit + "" // No strategy identifier needed for deposit + ); + + return requestId; + } + + /// @notice Withdraw from existing Tide + /// @param tideId The Tide ID to withdraw from + /// @param amount Amount to withdraw + function withdrawFromTide( + uint64 tideId, + uint256 amount + ) external onlyAllowlisted returns (uint256) { + if (amount == 0) revert AmountMustBeGreaterThanZero(); + _validateTideId(tideId, msg.sender); + _checkPendingRequestLimit(); + + uint256 requestId = createRequest( + RequestType.WITHDRAW_FROM_TIDE, + NATIVE_FLOW, // Assume FLOW for MVP + amount, + tideId, + "", // No vault identifier needed for withdraw + "" // No strategy identifier needed for withdraw + ); + + return requestId; + } + + /// @notice Close Tide and withdraw all funds + /// @param tideId The Tide ID to close + function closeTide( + uint64 tideId + ) external onlyAllowlisted returns (uint256) { + _validateTideId(tideId, msg.sender); + _checkPendingRequestLimit(); + + uint256 requestId = createRequest( + RequestType.CLOSE_TIDE, + NATIVE_FLOW, + 0, // Amount will be determined by Cadence + tideId, + "", // No vault identifier needed for close + "" // No strategy identifier needed for close + ); + + return requestId; + } + + /// @notice Cancel a pending request and reclaim funds + /// @param requestId The request ID to cancel + function cancelRequest(uint256 requestId) external { + Request storage request = pendingRequests[requestId]; + + if (request.id != requestId) revert RequestNotFound(); + if (request.user != msg.sender) revert NotRequestOwner(); + if (request.status != RequestStatus.PENDING) + revert CanOnlyCancelPending(); + + // Update status to FAILED with cancellation message + request.status = RequestStatus.FAILED; + request.message = "Cancelled"; + + // Decrement user pending request count + if (userPendingRequestCount[msg.sender] > 0) { + userPendingRequestCount[msg.sender]--; + } + + // Remove from pending queue + _removePendingRequest(requestId); + + // Refund funds if this was a CREATE_TIDE or DEPOSIT_TO_TIDE request + uint256 refundAmount = 0; + if ( + (request.requestType == RequestType.CREATE_TIDE || + request.requestType == RequestType.DEPOSIT_TO_TIDE) && + request.amount > 0 + ) { + refundAmount = request.amount; + + // Decrease pending balance + pendingUserBalances[msg.sender][request.tokenAddress] -= request + .amount; + emit BalanceUpdated( + msg.sender, + request.tokenAddress, + pendingUserBalances[msg.sender][request.tokenAddress] + ); + + // Refund the funds + if (isNativeFlow(request.tokenAddress)) { + (bool success, ) = msg.sender.call{value: request.amount}(""); + if (!success) revert TransferFailed(); + } else { + // TODO: Transfer ERC20 tokens (Phase 2) + revert ERC20NotSupported(); + } + + emit FundsWithdrawn( + msg.sender, + request.tokenAddress, + request.amount + ); + } + + emit RequestCancelled(requestId, msg.sender, refundAmount); + emit RequestProcessed( + requestId, + RequestStatus.FAILED, + request.tideId, + "Cancelled" + ); + } + + // ============================================ + // COA Functions (called by FlowVaultsEVM) + // ============================================ + + /// @notice Withdraw funds from contract (only authorized COA) + /// @param tokenAddress Token to withdraw + /// @param amount Amount to withdraw + function withdrawFunds( + address tokenAddress, + uint256 amount + ) external onlyAuthorizedCOA { + if (amount == 0) revert AmountMustBeGreaterThanZero(); + + if (isNativeFlow(tokenAddress)) { + if (address(this).balance < amount) revert InsufficientBalance(); + (bool success, ) = msg.sender.call{value: amount}(""); + if (!success) revert TransferFailed(); + } else { + // TODO: Transfer ERC20 tokens (Phase 2) + revert ERC20NotSupported(); + } + + emit FundsWithdrawn(msg.sender, tokenAddress, amount); + } + + /// @notice Update request status (only authorized COA) + /// @param requestId Request ID to update + /// @param status New status (as uint8: 0=PENDING, 1=COMPLETED, 2=FAILED) + /// @param tideId Associated Tide ID (if applicable) + /// @param message Status message (e.g., error reason if failed) + function updateRequestStatus( + uint256 requestId, + uint8 status, + uint64 tideId, + string calldata message + ) external onlyAuthorizedCOA { + Request storage request = pendingRequests[requestId]; + if (request.id != requestId) revert RequestNotFound(); + if (request.status != RequestStatus.PENDING) + revert RequestAlreadyFinalized(); + + // Convert uint8 to RequestStatus + request.status = RequestStatus(status); + request.message = message; + request.tideId = tideId; // Always update the tideId + // Register the new tide ID if this was a successful CREATE_TIDE + if ( + status == uint8(RequestStatus.COMPLETED) && + request.requestType == RequestType.CREATE_TIDE + ) { + validTideIds[tideId] = true; + tideOwners[tideId] = request.user; + emit TideIdRegistered(tideId); + } + + // Remove from pending queue and decrement counter (since we only transition from PENDING to COMPLETED/FAILED now) + // Decrement user pending request count + if (userPendingRequestCount[request.user] > 0) { + userPendingRequestCount[request.user]--; + } + _removePendingRequest(requestId); + + emit RequestProcessed( + requestId, + RequestStatus(status), + tideId, + message + ); + } + + /// @notice Update user balance (only authorized COA) + /// @param user User address + /// @param tokenAddress Token address + /// @param newBalance New balance + function updateUserBalance( + address user, + address tokenAddress, + uint256 newBalance + ) external onlyAuthorizedCOA { + pendingUserBalances[user][tokenAddress] = newBalance; + emit BalanceUpdated(user, tokenAddress, newBalance); + } + + // ============================================ + // View Functions + // ============================================ + + /// @notice Check if token is native FLOW + function isNativeFlow(address tokenAddress) public pure returns (bool) { + return tokenAddress == NATIVE_FLOW; + } + + /// @notice Get user's pending balance for a token + function getUserBalance( + address user, + address tokenAddress + ) external view returns (uint256) { + return pendingUserBalances[user][tokenAddress]; + } + + /// @notice Get count of pending requests (most gas-efficient) + function getPendingRequestCount() external view returns (uint256) { + return pendingRequestIds.length; + } + + /// @notice Get all pending request IDs (for counting/scheduling) + function getPendingRequestIds() external view returns (uint256[] memory) { + return pendingRequestIds; + } + + /// @notice Get pending requests unpacked with limit (OPTIMIZED for Cadence) + /// @param limit Maximum number of requests to return (0 = return all) + /// @return ids Array of request IDs + /// @return users Array of user addresses + /// @return requestTypes Array of request types + /// @return statuses Array of request statuses + /// @return tokenAddresses Array of token addresses + /// @return amounts Array of amounts + /// @return tideIds Array of tide IDs + /// @return timestamps Array of timestamps + /// @return messages Array of status messages + /// @return vaultIdentifiers Array of vault identifiers + /// @return strategyIdentifiers Array of strategy identifiers + function getPendingRequestsUnpacked( + uint256 limit + ) + external + view + returns ( + uint256[] memory ids, + address[] memory users, + uint8[] memory requestTypes, + uint8[] memory statuses, + address[] memory tokenAddresses, + uint256[] memory amounts, + uint64[] memory tideIds, + uint256[] memory timestamps, + string[] memory messages, + string[] memory vaultIdentifiers, + string[] memory strategyIdentifiers + ) + { + // Determine actual size: min(limit, total pending) + // If limit is 0, return all requests + uint256 size = limit == 0 + ? pendingRequestIds.length + : ( + limit < pendingRequestIds.length + ? limit + : pendingRequestIds.length + ); + + ids = new uint256[](size); + users = new address[](size); + requestTypes = new uint8[](size); + statuses = new uint8[](size); + tokenAddresses = new address[](size); + amounts = new uint256[](size); + tideIds = new uint64[](size); + timestamps = new uint256[](size); + messages = new string[](size); + vaultIdentifiers = new string[](size); + strategyIdentifiers = new string[](size); + + // Populate arrays up to size + for (uint256 i = 0; i < size; ) { + Request memory req = pendingRequests[pendingRequestIds[i]]; + ids[i] = req.id; + users[i] = req.user; + requestTypes[i] = uint8(req.requestType); + statuses[i] = uint8(req.status); + tokenAddresses[i] = req.tokenAddress; + amounts[i] = req.amount; + tideIds[i] = req.tideId; + timestamps[i] = req.timestamp; + messages[i] = req.message; + vaultIdentifiers[i] = req.vaultIdentifier; + strategyIdentifiers[i] = req.strategyIdentifier; + unchecked { + ++i; + } + } + } + + /// @notice Get specific request + function getRequest( + uint256 requestId + ) external view returns (Request memory) { + return pendingRequests[requestId]; + } + + /// @notice Check if a tide ID is valid + /// @param tideId The tide ID to check + /// @return True if the tide ID exists + function isTideIdValid(uint64 tideId) external view returns (bool) { + return validTideIds[tideId]; + } + + /// @notice Get user's pending request count + /// @param user The user address to check + /// @return Number of pending requests for the user + function getUserPendingRequestCount( + address user + ) external view returns (uint256) { + return userPendingRequestCount[user]; + } + + // ============================================ + // Internal Functions + // ============================================ + + /// @notice Validate token deposit (amount and msg.value) + /// @param tokenAddress Token being deposited + /// @param amount Amount being deposited + function _validateDeposit( + address tokenAddress, + uint256 amount + ) internal view { + if (amount == 0) revert AmountMustBeGreaterThanZero(); + + // Check minimum balance requirement + if (minimumBalance > 0 && amount < minimumBalance) { + revert BelowMinimumBalance(); + } + + if (isNativeFlow(tokenAddress)) { + if (msg.value != amount) revert MsgValueMustEqualAmount(); + } else { + if (msg.value != 0) revert MsgValueMustBeZero(); + // TODO: Transfer ERC20 tokens (Phase 2) + revert ERC20NotSupported(); + } + } + + /// @notice Validate that tide ID exists and caller is owner + /// @param tideId The tide ID to validate + /// @param user The expected owner address + function _validateTideId(uint64 tideId, address user) internal view { + if (!validTideIds[tideId] || tideOwners[tideId] != user) { + revert InvalidTideId(); + } + } + + /// @notice Check if user has exceeded pending request limit + function _checkPendingRequestLimit() internal view { + if ( + maxPendingRequestsPerUser > 0 && + userPendingRequestCount[msg.sender] >= maxPendingRequestsPerUser + ) { + revert TooManyPendingRequests(); + } + } + + function createRequest( + RequestType requestType, + address tokenAddress, + uint256 amount, + uint64 tideId, + string memory vaultIdentifier, + string memory strategyIdentifier + ) internal returns (uint256) { + address user = msg.sender; + uint256 requestId = _requestIdCounter++; + + Request memory newRequest = Request({ + id: requestId, + user: user, + requestType: requestType, + status: RequestStatus.PENDING, + tokenAddress: tokenAddress, + amount: amount, + tideId: tideId, + timestamp: block.timestamp, + message: "", // Empty message initially + vaultIdentifier: vaultIdentifier, + strategyIdentifier: strategyIdentifier + }); + + // Store in pending requests + pendingRequests[requestId] = newRequest; + pendingRequestIds.push(requestId); + + // Increment user pending request count + userPendingRequestCount[user]++; + + // Update pending user balance if depositing + if ( + requestType == RequestType.CREATE_TIDE || + requestType == RequestType.DEPOSIT_TO_TIDE + ) { + pendingUserBalances[user][tokenAddress] += amount; + emit BalanceUpdated( + user, + tokenAddress, + pendingUserBalances[user][tokenAddress] + ); + } + + emit RequestCreated( + requestId, + user, + requestType, + tokenAddress, + amount, + tideId + ); + + return requestId; + } + + function _removePendingRequest(uint256 requestId) internal { + // Find and remove from pendingRequestIds array + for (uint256 i = 0; i < pendingRequestIds.length; ) { + if (pendingRequestIds[i] == requestId) { + // Move last element to this position and pop + pendingRequestIds[i] = pendingRequestIds[ + pendingRequestIds.length - 1 + ]; + pendingRequestIds.pop(); + break; + } + unchecked { + ++i; + } + } + + // Don't delete from pendingRequests mapping to preserve history + // Just mark as completed/failed via status + } + + // ============================================ + // Receive Function + // ============================================ + + receive() external payable { + // Allow contract to receive ETH + } +} diff --git a/solidity/test/FlowVaultsRequests.t.sol b/solidity/test/FlowVaultsRequests.t.sol new file mode 100644 index 0000000..900cffc --- /dev/null +++ b/solidity/test/FlowVaultsRequests.t.sol @@ -0,0 +1,1001 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import "forge-std/Test.sol"; +import "../src/FlowVaultsRequests.sol"; + +// Test helper contract that exposes validTideIds for testing +contract FlowVaultsRequestsTestHelper is FlowVaultsRequests { + constructor(address coaAddress) FlowVaultsRequests(coaAddress) {} + + // Allow tests to directly register tide IDs without going through request flow + function testRegisterTideId(uint64 tideId, address owner) external { + validTideIds[tideId] = true; + tideOwners[tideId] = owner; + } +} + +contract FlowVaultsRequestsTest is Test { + FlowVaultsRequestsTestHelper public c; // Short name for brevity + address user = makeAddr("user"); + address coa = makeAddr("coa"); + address constant NATIVE_FLOW = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; + + // Test vault and strategy identifiers for testnet + string constant VAULT_IDENTIFIER = "A.7e60df042a9c0868.FlowToken.Vault"; + string constant STRATEGY_IDENTIFIER = + "A.3bda2f90274dbc9b.FlowVaultsStrategies.TracerStrategy"; + + // Event declarations for testing + event RequestCreated( + uint256 indexed requestId, + address indexed user, + FlowVaultsRequests.RequestType requestType, + address indexed tokenAddress, + uint256 amount, + uint64 tideId + ); + event BalanceUpdated( + address indexed user, + address indexed tokenAddress, + uint256 newBalance + ); + event RequestProcessed( + uint256 indexed requestId, + FlowVaultsRequests.RequestStatus status, + uint64 tideId, + string message + ); + event RequestCancelled( + uint256 indexed requestId, + address indexed user, + uint256 refundAmount + ); + event FundsWithdrawn( + address indexed to, + address indexed tokenAddress, + uint256 amount + ); + event AuthorizedCOAUpdated(address indexed oldCOA, address indexed newCOA); + + function setUp() public { + vm.deal(user, 100 ether); + c = new FlowVaultsRequestsTestHelper(coa); + + // Register commonly used tide IDs for testing + c.testRegisterTideId(0, user); // Tide ID 0 + c.testRegisterTideId(42, user); // Commonly used test tide ID + } + + // ============================================ + // 1. USER REQUEST CREATION TESTS + // ============================================ + + // CREATE_TIDE Tests + // ============================================ + function test_CreateTide() public { + vm.prank(user); + uint256 reqId = c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + assertEq(reqId, 1); + assertEq(c.getUserBalance(user, NATIVE_FLOW), 1 ether); + assertEq(c.getPendingRequestCount(), 1); + + FlowVaultsRequests.Request memory req = c.getRequest(reqId); + assertEq( + uint8(req.requestType), + uint8(FlowVaultsRequests.RequestType.CREATE_TIDE) + ); + } + + function test_CreateTide_RevertZeroAmount() public { + vm.prank(user); + vm.expectRevert( + FlowVaultsRequests.AmountMustBeGreaterThanZero.selector + ); + c.createTide{value: 0}( + NATIVE_FLOW, + 0, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + } + + function test_CreateTide_RevertMsgValueMismatch() public { + vm.prank(user); + vm.expectRevert(FlowVaultsRequests.MsgValueMustEqualAmount.selector); + c.createTide{value: 0.5 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); // Mismatch + } + + // DEPOSIT_TO_TIDE Tests + // ============================================ + function test_DepositToTide() public { + vm.prank(user); + uint256 reqId = c.depositToTide{value: 0.5 ether}( + 42, + NATIVE_FLOW, + 0.5 ether + ); + + assertEq(reqId, 1); + assertEq(c.getUserBalance(user, NATIVE_FLOW), 0.5 ether); + + FlowVaultsRequests.Request memory req = c.getRequest(reqId); + assertEq( + uint8(req.requestType), + uint8(FlowVaultsRequests.RequestType.DEPOSIT_TO_TIDE) + ); + assertEq(req.tideId, 42); + } + + function test_DepositToTide_TideIdZero() public { + // Tide ID 0 is valid (first tide created) + vm.prank(user); + uint256 reqId = c.depositToTide{value: 1 ether}( + 0, + NATIVE_FLOW, + 1 ether + ); + + assertEq(reqId, 1); + FlowVaultsRequests.Request memory req = c.getRequest(reqId); + assertEq(req.tideId, 0); + assertEq( + uint8(req.requestType), + uint8(FlowVaultsRequests.RequestType.DEPOSIT_TO_TIDE) + ); + } + + // WITHDRAW_FROM_TIDE Tests + // ============================================ + function test_WithdrawFromTide() public { + vm.prank(user); + uint256 reqId = c.withdrawFromTide(42, 0.3 ether); + + assertEq(reqId, 1); + assertEq(c.getPendingRequestCount(), 1); + + FlowVaultsRequests.Request memory req = c.getRequest(reqId); + assertEq( + uint8(req.requestType), + uint8(FlowVaultsRequests.RequestType.WITHDRAW_FROM_TIDE) + ); + assertEq(req.amount, 0.3 ether); + } + + // CLOSE_TIDE Tests + // ============================================ + function test_CloseTide() public { + vm.prank(user); + uint256 reqId = c.closeTide(42); + + assertEq(reqId, 1); + + FlowVaultsRequests.Request memory req = c.getRequest(reqId); + assertEq( + uint8(req.requestType), + uint8(FlowVaultsRequests.RequestType.CLOSE_TIDE) + ); + assertEq(req.tideId, 42); + } + + // ============================================ + // 2. REQUEST CANCELLATION TESTS + // ============================================ + function test_CancelRequest() public { + vm.startPrank(user); + uint256 reqId = c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + uint256 balBefore = user.balance; + c.cancelRequest(reqId); + + assertEq(user.balance, balBefore + 1 ether); // Refunded + assertEq(c.getUserBalance(user, NATIVE_FLOW), 0); + assertEq(c.getPendingRequestCount(), 0); + vm.stopPrank(); + } + + function test_CancelRequest_RevertNotOwner() public { + vm.prank(user); + uint256 reqId = c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + vm.prank(makeAddr("other")); + vm.expectRevert(); + c.cancelRequest(reqId); + } + + function test_DoubleRefund_Prevention() public { + // User creates tide + vm.prank(user); + uint256 reqId = c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + uint256 balBefore = user.balance; + + // User cancels and gets refund + vm.prank(user); + c.cancelRequest(reqId); + + assertEq(user.balance, balBefore + 1 ether); + + // Try to cancel again - should revert because request is now FAILED + vm.prank(user); + vm.expectRevert(FlowVaultsRequests.CanOnlyCancelPending.selector); + c.cancelRequest(reqId); + + // Balance should not have changed + assertEq(user.balance, balBefore + 1 ether); + } + + // ============================================ + // 3. COA OPERATIONS TESTS + // ============================================ + function test_COA_WithdrawFunds() public { + vm.prank(user); + c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + vm.prank(coa); + c.withdrawFunds(NATIVE_FLOW, 1 ether); + + assertEq(coa.balance, 1 ether); + } + + function test_COA_UpdateRequestStatus() public { + vm.prank(user); + c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + vm.prank(coa); + c.updateRequestStatus( + 1, + uint8(FlowVaultsRequests.RequestStatus.COMPLETED), + 42, + "Success" + ); + + FlowVaultsRequests.Request memory req = c.getRequest(1); + assertEq( + uint8(req.status), + uint8(FlowVaultsRequests.RequestStatus.COMPLETED) + ); + assertEq(req.tideId, 42); + assertEq(c.getPendingRequestCount(), 0); // Removed from pending + } + + function test_COA_UpdateUserBalance() public { + vm.prank(user); + c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + vm.prank(coa); + c.updateUserBalance(user, NATIVE_FLOW, 0.5 ether); + + assertEq(c.getUserBalance(user, NATIVE_FLOW), 0.5 ether); + } + + function test_COA_RevertUnauthorized() public { + vm.prank(user); + vm.expectRevert(); + c.withdrawFunds(NATIVE_FLOW, 1 ether); + } + + // ============================================ + // 4. QUERY & VIEW FUNCTIONS TESTS + // ============================================ + + function test_GetPendingRequestsUnpacked() public { + vm.startPrank(user); + c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + c.depositToTide{value: 0.5 ether}(42, NATIVE_FLOW, 0.5 ether); + vm.stopPrank(); + + ( + uint256[] memory ids, + address[] memory users, + , + , + , + uint256[] memory amounts, + , + , + , + , + + ) = c.getPendingRequestsUnpacked(0); + + assertEq(ids.length, 2); + assertEq(ids[0], 1); + assertEq(users[0], user); + assertEq(amounts[0], 1 ether); + } + + function test_GetPendingRequestsUnpacked_WithLimit() public { + vm.startPrank(user); + c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + c.createTide{value: 2 ether}( + NATIVE_FLOW, + 2 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + c.createTide{value: 3 ether}( + NATIVE_FLOW, + 3 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + vm.stopPrank(); + + (uint256[] memory ids, , , , , , , , , , ) = c + .getPendingRequestsUnpacked(2); + + assertEq(ids.length, 2); // Limited to 2 + } + + // ============================================ + // 5. MULTI-USER SCENARIOS + // ============================================ + + function test_MultipleUsers_SeparateBalances() public { + address user2 = makeAddr("user2"); + vm.deal(user2, 100 ether); + + // User 1 creates tide + vm.prank(user); + c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + // User 2 creates tide + vm.prank(user2); + c.createTide{value: 2 ether}( + NATIVE_FLOW, + 2 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + // Verify balances are separate + assertEq(c.getUserBalance(user, NATIVE_FLOW), 1 ether); + assertEq(c.getUserBalance(user2, NATIVE_FLOW), 2 ether); + + // Verify requests were created + FlowVaultsRequests.Request memory req1 = c.getRequest(1); + FlowVaultsRequests.Request memory req2 = c.getRequest(2); + assertEq(req1.user, user); + assertEq(req2.user, user2); + } + + function test_MultipleUsers_RequestIsolation() public { + address user2 = makeAddr("user2"); + vm.deal(user2, 100 ether); + + // User 1 creates tide + vm.prank(user); + uint256 reqId = c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + // User 2 tries to cancel User 1's request + vm.prank(user2); + vm.expectRevert(FlowVaultsRequests.NotRequestOwner.selector); + c.cancelRequest(reqId); + + // Verify request still exists + assertEq(c.getPendingRequestCount(), 1); + assertEq(c.getUserBalance(user, NATIVE_FLOW), 1 ether); + } + + // ============================================ + // 6. BALANCE & ACCOUNTING TESTS + // ============================================ + + function test_UserBalance_AfterFailedRequest() public { + // User creates tide + vm.prank(user); + uint256 reqId = c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + // Initial balance + assertEq(c.getUserBalance(user, NATIVE_FLOW), 1 ether); + + // COA marks request as failed (but doesn't update user balance) + vm.prank(coa); + c.updateRequestStatus( + reqId, + uint8(FlowVaultsRequests.RequestStatus.FAILED), + 0, + "Simulated failure" + ); + + // Balance should still be 1 ether (funds remain in contract) + assertEq(c.getUserBalance(user, NATIVE_FLOW), 1 ether); + + // Verify request is marked as failed + FlowVaultsRequests.Request memory req = c.getRequest(reqId); + assertEq( + uint8(req.status), + uint8(FlowVaultsRequests.RequestStatus.FAILED) + ); + + // Request is no longer in pending queue + assertEq(c.getPendingRequestCount(), 0); + + // Note: In a real scenario, the COA would need to update the user balance + // to return the funds, or the user would need a different mechanism to reclaim funds + // from failed requests that were already removed from pending queue + } + + // ============================================ + // 7. COMPLETE INTEGRATION FLOWS + // ============================================ + function test_FullCreateTideFlow() public { + // 1. User creates tide + vm.prank(user); + c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + // 2. COA processes + vm.startPrank(coa); + c.withdrawFunds(NATIVE_FLOW, 1 ether); + c.updateUserBalance(user, NATIVE_FLOW, 0); + c.updateRequestStatus( + 1, + uint8(FlowVaultsRequests.RequestStatus.COMPLETED), + 42, + "Tide created" + ); + vm.stopPrank(); + + // 3. Verify + assertEq(c.getUserBalance(user, NATIVE_FLOW), 0); + assertEq(c.getPendingRequestCount(), 0); + FlowVaultsRequests.Request memory req = c.getRequest(1); + assertEq(req.tideId, 42); + } + + function test_FullWithdrawFlow() public { + // User withdraws from existing tide + vm.prank(user); + c.withdrawFromTide(42, 0.5 ether); + + // COA processes and sends funds back + vm.deal(address(c), 0.5 ether); + vm.startPrank(coa); + // In real scenario, COA would bridge funds back to user's EVM address + c.updateRequestStatus( + 1, + uint8(FlowVaultsRequests.RequestStatus.COMPLETED), + 42, + "Withdrawn" + ); + vm.stopPrank(); + + FlowVaultsRequests.Request memory req = c.getRequest(1); + assertEq( + uint8(req.status), + uint8(FlowVaultsRequests.RequestStatus.COMPLETED) + ); + } + + // ============================================ + // 8. EVENT EMISSION TESTS + // ============================================ + + function test_Events_RequestCreated() public { + vm.prank(user); + + vm.expectEmit(true, true, true, true); + emit RequestCreated( + 1, // requestId + user, + FlowVaultsRequests.RequestType.CREATE_TIDE, + NATIVE_FLOW, + 1 ether, + 0 // tideId + ); + + c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + } + + function test_Events_BalanceUpdated() public { + vm.prank(user); + + vm.expectEmit(true, true, false, true); + emit BalanceUpdated(user, NATIVE_FLOW, 1 ether); + + c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + } + + function test_Events_RequestProcessed() public { + vm.prank(user); + c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + vm.prank(coa); + + vm.expectEmit(true, false, false, true); + emit RequestProcessed( + 1, + FlowVaultsRequests.RequestStatus.COMPLETED, + 42, + "Success" + ); + + c.updateRequestStatus( + 1, + uint8(FlowVaultsRequests.RequestStatus.COMPLETED), + 42, + "Success" + ); + } + + function test_Events_RequestCancelled() public { + vm.prank(user); + uint256 reqId = c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + vm.prank(user); + + vm.expectEmit(true, true, false, true); + emit RequestCancelled(reqId, user, 1 ether); + + c.cancelRequest(reqId); + } + + function test_Events_FundsWithdrawn() public { + vm.prank(user); + c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + + vm.prank(coa); + + vm.expectEmit(true, true, false, true); + emit FundsWithdrawn(coa, NATIVE_FLOW, 1 ether); + + c.withdrawFunds(NATIVE_FLOW, 1 ether); + } + + function test_Events_AuthorizedCOAUpdated() public { + address newCOA = makeAddr("newCOA"); + + vm.prank(c.owner()); + + vm.expectEmit(true, true, false, true); + emit AuthorizedCOAUpdated(coa, newCOA); + + c.setAuthorizedCOA(newCOA); + } + + // ============================================ + // ALLOW LIST TESTS + // ============================================ + + event AllowlistEnabled(bool enabled); + event AddressesAddedToAllowlist(address[] addresses); + event AddressesRemovedFromAllowlist(address[] addresses); + + function test_Allowlist_InitialState() public view { + assertFalse(c.allowlistEnabled()); + assertFalse(c.allowlisted(user)); + } + + function test_Allowlist_SetEnabled() public { + vm.prank(c.owner()); + c.setAllowlistEnabled(true); + assertTrue(c.allowlistEnabled()); + + vm.prank(c.owner()); + c.setAllowlistEnabled(false); + assertFalse(c.allowlistEnabled()); + } + + function test_Allowlist_SetEnabled_RevertNonOwner() public { + vm.prank(user); + vm.expectRevert(FlowVaultsRequests.NotOwner.selector); + c.setAllowlistEnabled(true); + } + + function test_Allowlist_BatchAdd_SingleAddress() public { + address[] memory addresses = new address[](1); + addresses[0] = user; + + vm.prank(c.owner()); + c.batchAddToAllowlist(addresses); + + assertTrue(c.allowlisted(user)); + } + + function test_Allowlist_BatchAdd_MultipleAddresses() public { + address user2 = makeAddr("user2"); + address user3 = makeAddr("user3"); + + address[] memory addresses = new address[](3); + addresses[0] = user; + addresses[1] = user2; + addresses[2] = user3; + + vm.prank(c.owner()); + c.batchAddToAllowlist(addresses); + + assertTrue(c.allowlisted(user)); + assertTrue(c.allowlisted(user2)); + assertTrue(c.allowlisted(user3)); + } + + function test_Allowlist_BatchAdd_RevertEmptyArray() public { + address[] memory addresses = new address[](0); + + vm.prank(c.owner()); + vm.expectRevert(FlowVaultsRequests.EmptyAddressArray.selector); + c.batchAddToAllowlist(addresses); + } + + function test_Allowlist_BatchAdd_RevertZeroAddress() public { + address[] memory addresses = new address[](2); + addresses[0] = user; + addresses[1] = address(0); + + vm.prank(c.owner()); + vm.expectRevert(FlowVaultsRequests.CannotAllowlistZeroAddress.selector); + c.batchAddToAllowlist(addresses); + } + + function test_Allowlist_BatchAdd_RevertNonOwner() public { + address[] memory addresses = new address[](1); + addresses[0] = user; + + vm.prank(user); + vm.expectRevert(FlowVaultsRequests.NotOwner.selector); + c.batchAddToAllowlist(addresses); + } + + function test_Allowlist_BatchRemove_SingleAddress() public { + // First add user to allow list + address[] memory addresses = new address[](1); + addresses[0] = user; + + vm.prank(c.owner()); + c.batchAddToAllowlist(addresses); + assertTrue(c.allowlisted(user)); + + // Now remove + vm.prank(c.owner()); + c.batchRemoveFromAllowlist(addresses); + assertFalse(c.allowlisted(user)); + } + + function test_Allowlist_BatchRemove_MultipleAddresses() public { + address user2 = makeAddr("user2"); + address user3 = makeAddr("user3"); + + address[] memory addresses = new address[](3); + addresses[0] = user; + addresses[1] = user2; + addresses[2] = user3; + + // Add all + vm.prank(c.owner()); + c.batchAddToAllowlist(addresses); + + // Remove all + vm.prank(c.owner()); + c.batchRemoveFromAllowlist(addresses); + + assertFalse(c.allowlisted(user)); + assertFalse(c.allowlisted(user2)); + assertFalse(c.allowlisted(user3)); + } + + function test_Allowlist_BatchRemove_RevertEmptyArray() public { + address[] memory addresses = new address[](0); + + vm.prank(c.owner()); + vm.expectRevert(FlowVaultsRequests.EmptyAddressArray.selector); + c.batchRemoveFromAllowlist(addresses); + } + + function test_Allowlist_BatchRemove_RevertNonOwner() public { + address[] memory addresses = new address[](1); + addresses[0] = user; + + vm.prank(user); + vm.expectRevert(FlowVaultsRequests.NotOwner.selector); + c.batchRemoveFromAllowlist(addresses); + } + + function test_Allowlist_CreateTide_AllowlistDisabled() public { + // Allow list is disabled by default, so anyone can create + vm.prank(user); + uint256 reqId = c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + assertEq(reqId, 1); + } + + function test_Allowlist_CreateTide_AllowlistEnabled_NotInAllowlist() + public + { + vm.prank(c.owner()); + c.setAllowlistEnabled(true); + + vm.prank(user); + vm.expectRevert(FlowVaultsRequests.NotInAllowlist.selector); + c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + } + + function test_Allowlist_CreateTide_AllowlistEnabled_InAllowlist() public { + // Add user to allow list + address[] memory addresses = new address[](1); + addresses[0] = user; + + vm.prank(c.owner()); + c.batchAddToAllowlist(addresses); + + // Enable allow list + vm.prank(c.owner()); + c.setAllowlistEnabled(true); + + // User should be able to create tide + vm.prank(user); + uint256 reqId = c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + assertEq(reqId, 1); + } + + function test_Allowlist_DepositToTide_AllowlistEnabled_NotInAllowlist() + public + { + vm.prank(c.owner()); + c.setAllowlistEnabled(true); + + vm.prank(user); + vm.expectRevert(FlowVaultsRequests.NotInAllowlist.selector); + c.depositToTide{value: 1 ether}(42, NATIVE_FLOW, 1 ether); + } + + function test_Allowlist_DepositToTide_AllowlistEnabled_InAllowlist() + public + { + address[] memory addresses = new address[](1); + addresses[0] = user; + + vm.prank(c.owner()); + c.batchAddToAllowlist(addresses); + + vm.prank(c.owner()); + c.setAllowlistEnabled(true); + + vm.prank(user); + uint256 reqId = c.depositToTide{value: 1 ether}( + 42, + NATIVE_FLOW, + 1 ether + ); + assertEq(reqId, 1); + } + + function test_Allowlist_WithdrawFromTide_AllowlistEnabled_NotInAllowlist() + public + { + vm.prank(c.owner()); + c.setAllowlistEnabled(true); + + vm.prank(user); + vm.expectRevert(FlowVaultsRequests.NotInAllowlist.selector); + c.withdrawFromTide(42, 1 ether); + } + + function test_Allowlist_WithdrawFromTide_AllowlistEnabled_InAllowlist() + public + { + address[] memory addresses = new address[](1); + addresses[0] = user; + + vm.prank(c.owner()); + c.batchAddToAllowlist(addresses); + + vm.prank(c.owner()); + c.setAllowlistEnabled(true); + + vm.prank(user); + uint256 reqId = c.withdrawFromTide(42, 1 ether); + assertEq(reqId, 1); + } + + function test_Allowlist_CloseTide_AllowlistEnabled_NotInAllowlist() public { + vm.prank(c.owner()); + c.setAllowlistEnabled(true); + + vm.prank(user); + vm.expectRevert(FlowVaultsRequests.NotInAllowlist.selector); + c.closeTide(42); + } + + function test_Allowlist_CloseTide_AllowlistEnabled_InAllowlist() public { + address[] memory addresses = new address[](1); + addresses[0] = user; + + vm.prank(c.owner()); + c.batchAddToAllowlist(addresses); + + vm.prank(c.owner()); + c.setAllowlistEnabled(true); + + vm.prank(user); + uint256 reqId = c.closeTide(42); + assertEq(reqId, 1); + } + + function test_Allowlist_RemoveAfterAdd() public { + address[] memory addresses = new address[](1); + addresses[0] = user; + + // Add + vm.prank(c.owner()); + c.batchAddToAllowlist(addresses); + assertTrue(c.allowlisted(user)); + + // Enable allow list + vm.prank(c.owner()); + c.setAllowlistEnabled(true); + + // User can create tide + vm.prank(user); + uint256 reqId = c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + assertEq(reqId, 1); + + // Remove from allow list + vm.prank(c.owner()); + c.batchRemoveFromAllowlist(addresses); + assertFalse(c.allowlisted(user)); + + // User cannot create tide anymore + vm.prank(user); + vm.expectRevert(FlowVaultsRequests.NotInAllowlist.selector); + c.createTide{value: 1 ether}( + NATIVE_FLOW, + 1 ether, + VAULT_IDENTIFIER, + STRATEGY_IDENTIFIER + ); + } + + function test_Allowlist_Events_AllowlistEnabled() public { + vm.prank(c.owner()); + + vm.expectEmit(false, false, false, true); + emit AllowlistEnabled(true); + + c.setAllowlistEnabled(true); + } + + function test_Allowlist_Events_AddressesAdded() public { + address[] memory addresses = new address[](2); + addresses[0] = user; + addresses[1] = makeAddr("user2"); + + vm.prank(c.owner()); + + vm.expectEmit(true, false, false, true); + emit AddressesAddedToAllowlist(addresses); + + c.batchAddToAllowlist(addresses); + } + + function test_Allowlist_Events_AddressesRemoved() public { + address[] memory addresses = new address[](2); + addresses[0] = user; + addresses[1] = makeAddr("user2"); + + vm.prank(c.owner()); + c.batchAddToAllowlist(addresses); + + vm.prank(c.owner()); + + vm.expectEmit(true, false, false, true); + emit AddressesRemovedFromAllowlist(addresses); + + c.batchRemoveFromAllowlist(addresses); + } +}