diff --git a/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md b/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md index 74bf117..3e8d251 100644 --- a/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md +++ b/FLOW_YIELD_VAULTS_EVM_BRIDGE_DESIGN.md @@ -547,7 +547,8 @@ function getPendingRequestIds() returns (uint256[] memory); // Get pending requests in unpacked arrays (pagination) function getPendingRequestsUnpacked(uint256 startIndex, uint256 count) returns (...); -// Get pending requests for a user in unpacked arrays (includes native FLOW balances) +// Get pending requests for a user in unpacked arrays (includes tracked-token escrow+refund balances) +// Returns (..., address[] memory balanceTokens, uint256[] memory pendingBalances, uint256[] memory claimableRefundsArray) function getPendingRequestsByUserUnpacked(address user) returns (...); // Get single request by ID diff --git a/FRONTEND_INTEGRATION.md b/FRONTEND_INTEGRATION.md index e05ede7..73c3a26 100644 --- a/FRONTEND_INTEGRATION.md +++ b/FRONTEND_INTEGRATION.md @@ -180,12 +180,19 @@ const [ messages, vaultIdentifiers, strategyIdentifiers, - pendingBalance, - claimableRefund, + balanceTokens, + pendingBalances, + claimableRefunds, ] = await contract.getPendingRequestsByUserUnpacked(userAddress); -// pendingBalance = escrowed funds for active pending requests (native FLOW only) -// claimableRefund = funds available to claim via claimRefund() (native FLOW only) -// Use getUserPendingBalance/getClaimableRefund for a specific token + +// `balanceTokens` contains the tracked token set configured on the contract. +// The balance arrays are aligned by index and may include disabled tokens if users still have balances/refunds. +const pendingByToken = Object.fromEntries( + balanceTokens.map((token, i) => [token, pendingBalances[i]]) +); +const claimableByToken = Object.fromEntries( + balanceTokens.map((token, i) => [token, claimableRefunds[i]]) +); ``` #### Get All Pending Requests (Paginated, Admin) diff --git a/cadence/contracts/FlowYieldVaultsEVM.cdc b/cadence/contracts/FlowYieldVaultsEVM.cdc index 64cf72f..bc6b009 100644 --- a/cadence/contracts/FlowYieldVaultsEVM.cdc +++ b/cadence/contracts/FlowYieldVaultsEVM.cdc @@ -132,15 +132,17 @@ access(all) contract FlowYieldVaultsEVM { access(all) struct PendingRequestsInfo { access(all) let evmAddress: String access(all) let pendingCount: Int - access(all) let pendingBalance: UFix64 - access(all) let claimableRefund: UFix64 + /// @notice Pending balances per token address (maps token address string -> UFix64 balance) + access(all) let pendingBalances: {String: UFix64} + /// @notice Claimable refunds per token address (maps token address string -> UFix64 balance) + access(all) let claimableRefunds: {String: UFix64} access(all) let requests: [EVMRequest] - init(evmAddress: String, pendingCount: Int, pendingBalance: UFix64, claimableRefund: UFix64, requests: [EVMRequest]) { + init(evmAddress: String, pendingCount: Int, pendingBalances: {String: UFix64}, claimableRefunds: {String: UFix64}, requests: [EVMRequest]) { self.evmAddress = evmAddress self.pendingCount = pendingCount - self.pendingBalance = pendingBalance - self.claimableRefund = claimableRefund + self.pendingBalances = pendingBalances + self.claimableRefunds = claimableRefunds self.requests = requests } } @@ -1701,8 +1703,9 @@ access(all) contract FlowYieldVaultsEVM { Type<[String]>(), // messages Type<[String]>(), // vaultIdentifiers Type<[String]>(), // strategyIdentifiers - Type(), // pendingBalance - Type() // claimableRefund + Type<[EVM.EVMAddress]>(), // balanceTokens + Type<[UInt256]>(), // pendingBalances + Type<[UInt256]>() // claimableRefundsArray ], data: callResult.data ) @@ -1717,12 +1720,38 @@ access(all) contract FlowYieldVaultsEVM { let messages = decoded[7] as! [String] let vaultIdentifiers = decoded[8] as! [String] let strategyIdentifiers = decoded[9] as! [String] - let pendingBalanceRaw = decoded[10] as! UInt256 - let claimableRefundRaw = decoded[11] as! UInt256 + let balanceTokens = decoded[10] as! [EVM.EVMAddress] + let pendingBalancesRaw = decoded[11] as! [UInt256] + let claimableRefundsRaw = decoded[12] as! [UInt256] - // Convert pending balance from wei to UFix64 - let pendingBalance = FlowEVMBridgeUtils.uint256ToUFix64(value: pendingBalanceRaw, decimals: 18) - let claimableRefund = FlowEVMBridgeUtils.uint256ToUFix64(value: claimableRefundRaw, decimals: 18) + assert( + balanceTokens.length == pendingBalancesRaw.length + && balanceTokens.length == claimableRefundsRaw.length, + message: "Balance array length mismatch in ABI decode" + ) + + // Build per-token balance dictionaries + var pendingBalances: {String: UFix64} = {} + var claimableRefundsMap: {String: UFix64} = {} + var j = 0 + while j < balanceTokens.length { + let tokenAddr = balanceTokens[j].toString() + let rawPending = pendingBalancesRaw[j] + let rawRefund = claimableRefundsRaw[j] + pendingBalances[tokenAddr] = rawPending == 0 + ? 0.0 + : FlowYieldVaultsEVM.ufix64FromUInt256( + rawPending, + tokenAddress: balanceTokens[j] + ) + claimableRefundsMap[tokenAddr] = rawRefund == 0 + ? 0.0 + : FlowYieldVaultsEVM.ufix64FromUInt256( + rawRefund, + tokenAddress: balanceTokens[j] + ) + j = j + 1 + } // Build request array var requests: [EVMRequest] = [] @@ -1748,8 +1777,8 @@ access(all) contract FlowYieldVaultsEVM { return PendingRequestsInfo( evmAddress: evmAddressHex, pendingCount: ids.length, - pendingBalance: pendingBalance, - claimableRefund: claimableRefund, + pendingBalances: pendingBalances, + claimableRefunds: claimableRefundsMap, requests: requests ) } @@ -1947,7 +1976,10 @@ access(all) contract FlowYieldVaultsEVM { /// @notice Converts a UInt256 amount from EVM to UFix64 for Cadence /// @dev For native FLOW: Uses 18 decimals (attoflow to FLOW conversion) - /// For ERC20: Uses FlowEVMBridgeUtils to look up token decimals + /// For ERC20: Uses FlowEVMBridgeUtils to look up token decimals. + /// Cadence UFix64 preserves 8 decimal places, so tokens with more than 8 + /// decimals are truncated toward zero when entering Cadence. Any remainder + /// smaller than 0.00000001 token is lost and cannot be recovered later. /// @param value The amount in wei/smallest unit (UInt256) /// @param tokenAddress The token address to determine decimal conversion /// @return The converted amount in UFix64 format @@ -1960,7 +1992,9 @@ access(all) contract FlowYieldVaultsEVM { /// @notice Converts a UFix64 amount from Cadence to UInt256 for EVM /// @dev For native FLOW: Uses 18 decimals (FLOW to attoflow conversion) - /// For ERC20: Uses FlowEVMBridgeUtils to look up token decimals + /// For ERC20: Uses FlowEVMBridgeUtils to look up token decimals. + /// This reconstructs an EVM amount from the already-quantized UFix64 value, + /// so previously truncated sub-1e-8 dust does not reappear on the return path. /// @param value The amount in UFix64 format /// @param tokenAddress The token address to determine decimal conversion /// @return The converted amount in wei/smallest unit (UInt256) diff --git a/cadence/tests/evm_bridge_lifecycle_test.cdc b/cadence/tests/evm_bridge_lifecycle_test.cdc index 238b525..6babcf9 100644 --- a/cadence/tests/evm_bridge_lifecycle_test.cdc +++ b/cadence/tests/evm_bridge_lifecycle_test.cdc @@ -255,6 +255,64 @@ fun testProcessResultStructure() { Test.assertEqual("Insufficient COA balance", failureResult.message) } +access(all) +fun testGetPendingRequestsForEVMAddressDecodesBalances() { + let tokenBAddress = deployERC20DecimalsOnlyMock(admin, decimals: 6) + let tokenCAddress = "0x00000000000000000000000000000000000000cc" + let mockAddress = deployPendingRequestsByUserQueryMock(admin, tokenBAddress: tokenBAddress, tokenCAddress: tokenCAddress) + let tokenBKey = EVM.addressFromString(tokenBAddress).toString() + let tokenCKey = EVM.addressFromString(tokenCAddress).toString() + let setAddrResult = updateRequestsAddress(admin, mockAddress) + Test.expect(setAddrResult, Test.beSucceeded()) + + let result = _executeScript( + "../scripts/get_pending_requests_for_evm_address.cdc", + [userEVMAddr1.toString()] + ) + Test.assertEqual(Test.ResultStatus.succeeded, result.status) + + let pendingInfo = result.returnValue as! FlowYieldVaultsEVM.PendingRequestsInfo + let nativeFlowKey = nativeFlowAddr.toString() + let pendingNative = pendingInfo.pendingBalances[nativeFlowKey] + ?? panic("Missing NATIVE_FLOW pending balance entry") + let refundNative = pendingInfo.claimableRefunds[nativeFlowKey] + ?? panic("Missing NATIVE_FLOW refund balance entry") + let pendingTokenB = pendingInfo.pendingBalances[tokenBKey] + ?? panic("Missing tokenB pending balance entry") + let refundTokenB = pendingInfo.claimableRefunds[tokenBKey] + ?? panic("Missing tokenB refund balance entry") + let pendingTokenC = pendingInfo.pendingBalances[tokenCKey] + ?? panic("Missing tokenC pending balance entry") + let refundTokenC = pendingInfo.claimableRefunds[tokenCKey] + ?? panic("Missing tokenC refund balance entry") + + Test.assertEqual(userEVMAddr1.toString(), pendingInfo.evmAddress) + Test.assertEqual(2, pendingInfo.pendingCount) + Test.assertEqual(3.0, pendingNative) + Test.assertEqual(0.0, refundNative) + Test.assertEqual(1.234567, pendingTokenB) + Test.assertEqual(0.5, refundTokenB) + Test.assertEqual(0.0, pendingTokenC) + Test.assertEqual(0.0, refundTokenC) + Test.assertEqual(2, pendingInfo.requests.length) + + Test.assertEqual(11 as UInt256, pendingInfo.requests[0].id) + Test.assertEqual(12 as UInt256, pendingInfo.requests[1].id) + Test.assertEqual(1000000000000000000 as UInt256, pendingInfo.requests[0].amount) + Test.assertEqual(1234567 as UInt256, pendingInfo.requests[1].amount) + Test.assertEqual(nil, pendingInfo.requests[0].yieldVaultId) + Test.assertEqual(42 as UInt64?, pendingInfo.requests[1].yieldVaultId) + Test.assertEqual(tokenBKey, pendingInfo.requests[1].tokenAddress.toString()) + Test.assertEqual( + FlowYieldVaultsEVM.RequestType.CREATE_YIELDVAULT.rawValue, + pendingInfo.requests[0].requestType + ) + Test.assertEqual( + FlowYieldVaultsEVM.RequestType.DEPOSIT_TO_YIELDVAULT.rawValue, + pendingInfo.requests[1].requestType + ) +} + access(all) fun testVaultAndStrategyIdentifiers() { // Test that vault and strategy identifiers are preserved correctly diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 6677e06..2482cc3 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -26,6 +26,16 @@ access(all) let falseApproveTokenBytecode = "608080604052346100155760bd908161001 access(all) let mockVaultIdentifier = "A.0ae53cb6e3f42a79.FlowToken.Vault" access(all) let mockStrategyIdentifier = "A.045a1763c93006ca.MockStrategies.TracerStrategy" +/* --- Embedded EVM mock bytecode --- */ + +// Regenerate with the repo's Foundry defaults from solidity/foundry.toml: +// `cd solidity && forge inspect src/test/PendingRequestsByUserQueryMock.sol:ERC20DecimalsOnlyMock bytecode` +access(all) let erc20DecimalsOnlyMockBytecode = "60a03461006257601f61011338819003918201601f19168301916001600160401b0383118484101761006657808492602094604052833981010312610062575160ff81168103610062576080526040516098908161007b823960805181603a0152f35b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe60808060405260043610156011575f80fd5b5f90813560e01c63313ce567146025575f80fd5b34605e5781600319360112605e5760209060ff7f0000000000000000000000000000000000000000000000000000000000000000168152f35b5080fdfea26469706673582212203c866ad60ad80831e09284cb4b396975c6e952f53d1755f246b0f36dcd9c903664736f6c63430008140033" + +// Regenerate with the repo's Foundry defaults from solidity/foundry.toml: +// `cd solidity && forge inspect src/test/PendingRequestsByUserQueryMock.sol:PendingRequestsByUserQueryMock bytecode` +access(all) let pendingRequestsByUserQueryMockBytecode = "60c03461008157601f6108dc38819003918201601f19168301916001600160401b0383118484101761008557808492604094855283398101031261008157610052602061004b83610099565b9201610099565b9060805260a05260405161082e90816100ae82396080518181816103f00152610681015260a0518161042e0152f35b5f80fd5b634e487b7160e01b5f52604160045260245ffd5b51906001600160a01b03821682036100815756fe60806040526004361015610011575f80fd5b5f803560e01c636b67542814610025575f80fd5b346101875760209081600319360112610187576004356001600160a01b0381168103610183576100549061038c565b9b9c94969d939792989199909a604051809e6101a080835282016100779161018a565b9086818303910152610088916101bd565b8d810360408f0152610099916101bd565b8c810360608e01526100aa916101f3565b8b810360808d01526100bb9161018a565b908a820360a08c015280808d5193848152019c01925b828110610165575050505093610134610152946101256101619895610116610143966101088f8f9e9c8f60c081840391015261018a565b8d810360e08f01529061022f565b908b82036101008d015261022f565b908982036101208b015261022f565b908782036101408901526101f3565b9085820361016087015261018a565b9083820361018085015261018a565b0390f35b835167ffffffffffffffff168d529b81019b928101926001016100d1565b5080fd5b80fd5b9081518082526020808093019301915f5b8281106101a9575050505090565b83518552938101939281019260010161019b565b9081518082526020808093019301915f5b8281106101dc575050505090565b835160ff16855293810193928101926001016101ce565b9081518082526020808093019301915f5b828110610212575050505090565b83516001600160a01b031685529381019392810192600101610204565b9080825180825260208092019180808360051b8601019501935f9384915b84831061025e575050505050505090565b9091929394959684601f198084840301855289518051908185528a5b8281106102a657505080840183018a9052601f011690910181019781019695949360010192019061024d565b81810185015186820186015289940161027a565b604051906020820182811067ffffffffffffffff8211176102da57604052565b634e487b7160e01b5f52604160045260245ffd5b604051906060820182811067ffffffffffffffff8211176102da57604052565b604051906080820182811067ffffffffffffffff8211176102da57604052565b80511561033b5760200190565b634e487b7160e01b5f52603260045260245ffd5b80516001101561033b5760400190565b6103676102ee565b9060028252815f5b6040811061037b575050565b80606060208093850101520161036f565b9061039561030e565b90600382526060366020840137816103ab61030e565b60038152606036602083013780916103c161030e565b9060038252606036602084013790958691906001600160a01b036103e48361032e565b526103ee8261034f565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316905281516002101561033b576001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000811660608401521660101901610769575050506104686102ee565b60028152916040366020850137829461047f6102ee565b6002815290604036602084013781956104966102ee565b60028152604036602083013780966104ac6102ee565b6002815292604036602086013783976104c36102ee565b6002815295604036602089013786986104da6102ee565b600281529760403660208b013788996104f16102ee565b600281526040366020830137809a61050761035f565b9a8b61051161035f565b9b8c9561051c61035f565b9c6105268161032e565b600b90525f978894856105388561032e565b52856105438661032e565b526001600160a01b036105558861032e565b5261055f8b61032e565b670de0b6b3a764000090526105738861032e565b67ffffffffffffffff90526105878961032e565b606490526105936102ba565b86815261059f8261032e565b526105a99061032e565b506105b26102ee565b602281527f412e306165353363623665336634326137392e466c6f77546f6b656e2e5661756020820152611b1d60f21b60408201526105f08261032e565b526105fa9061032e565b508d6106046102ee565b603081527f412e303435613137363363393330303663612e4d6f636b53747261746567696560208201526f732e547261636572537472617465677960801b60408201526106508261032e565b5261065a9061032e565b506106649061034f565b600c90526106719061034f565b6001905261067e9061034f565b527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316906106b39061034f565b526212d6876106c2819561034f565b526106cc9061034f565b602a90526106d99061034f565b606590526106e56102ba565b8181526106f18a61034f565b526106fb8961034f565b506107046102ba565b8181526107108961034f565b5261071a8861034f565b506107236102ba565b90815261072f8761034f565b526107398661034f565b506107438461032e565b6729a2241af62c000090526107578461034f565b526107618261034f565b6207a1209052565b925092945092506107786102ba565b925f928385526107866102ba565b958487526107926102ba565b9585875261079e6102ba565b958087526107aa6102ba565b958187526107b66102ba565b958287526107c26102ba565b958387526107ce6102ba565b958487526107da6102ba565b958587526107e66102ba565b9586529c9b9a9998979695949392919056fea2646970667358221220f90ed4a66eaa118955dbeba6ec133f3d37c612ed4d77722dcf4cd4ba51d5b4a464736f6c63430008140033" + /* --- Setup helpers --- */ // Deploys all required contracts for FlowYieldVaultsEVM @@ -291,6 +301,29 @@ fun deployEVMContract(_ signer: Test.TestAccount, _ bytecode: String, _ gasLimit return contractAddress } +access(all) +fun deployEVMContractWithArgs(_ signer: Test.TestAccount, _ bytecode: String, _ args: [AnyStruct]): String { + let argsBytecode = EVM.encodeABI(args) + let bytecodeWithArgs = String.encodeHex(bytecode.decodeHex().concat(argsBytecode)) + return deployEVMContract(signer, bytecodeWithArgs, UInt64(15_000_000)) +} + +access(all) +fun deployERC20DecimalsOnlyMock(_ signer: Test.TestAccount, decimals: UInt8): String { + return deployEVMContractWithArgs(signer, erc20DecimalsOnlyMockBytecode, [decimals]) +} + +access(all) +fun deployPendingRequestsByUserQueryMock( + _ signer: Test.TestAccount, + tokenBAddress: String, + tokenCAddress: String +): String { + let tokenB = EVM.addressFromString(tokenBAddress) + let tokenC = EVM.addressFromString(tokenCAddress) + return deployEVMContractWithArgs(signer, pendingRequestsByUserQueryMockBytecode, [tokenB, tokenC]) +} + access(all) fun deployFalseApproveToken(_ signer: Test.TestAccount): String { // Reuse the generic deploy helper so the regression test exercises the same COA diff --git a/deployments/artifacts/FlowYieldVaultsRequests.json b/deployments/artifacts/FlowYieldVaultsRequests.json index 88f282c..32fc49f 100644 --- a/deployments/artifacts/FlowYieldVaultsRequests.json +++ b/deployments/artifacts/FlowYieldVaultsRequests.json @@ -19,6 +19,19 @@ "type": "receive", "stateMutability": "payable" }, + { + "type": "function", + "name": "DEFAULT_MINIMUM_BALANCE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "NATIVE_FLOW", @@ -532,14 +545,19 @@ "internalType": "string[]" }, { - "name": "pendingBalance", - "type": "uint256", - "internalType": "uint256" + "name": "balanceTokens", + "type": "address[]", + "internalType": "address[]" }, { - "name": "claimableRefund", - "type": "uint256", - "internalType": "uint256" + "name": "pendingBalances", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "claimableRefundsArray", + "type": "uint256[]", + "internalType": "uint256[]" } ], "stateMutability": "view" @@ -1947,6 +1965,11 @@ "name": "CannotAllowlistZeroAddress", "inputs": [] }, + { + "type": "error", + "name": "CannotBlocklistZeroAddress", + "inputs": [] + }, { "type": "error", "name": "CannotRegisterSentinelYieldVaultId", @@ -1998,6 +2021,11 @@ "name": "InvalidCOAAddress", "inputs": [] }, + { + "type": "error", + "name": "InvalidMinimumBalance", + "inputs": [] + }, { "type": "error", "name": "InvalidRequestState", diff --git a/scripts/export-artifacts.sh b/scripts/export-artifacts.sh index ce9107d..3f49544 100755 --- a/scripts/export-artifacts.sh +++ b/scripts/export-artifacts.sh @@ -60,7 +60,7 @@ fi echo -e "${GREEN}Exporting contract artifacts...${NC}" # Create directories if they don't exist -mkdir -p deployments/artifacts +mkdir -p deployments/artifacts solidity/deployments/artifacts # Build contracts echo -e "${YELLOW}Building contracts...${NC}" @@ -68,9 +68,14 @@ cd solidity && forge build && cd .. # Extract ABI echo -e "${YELLOW}Extracting FlowYieldVaultsRequests ABI...${NC}" -jq '.abi' solidity/out/FlowYieldVaultsRequests.sol/FlowYieldVaultsRequests.json > deployments/artifacts/FlowYieldVaultsRequests.json +CANONICAL_ABI_PATH="deployments/artifacts/FlowYieldVaultsRequests.json" +SOLIDITY_ABI_PATH="solidity/deployments/artifacts/FlowYieldVaultsRequests.json" -echo -e "${GREEN}✓ ABI exported to deployments/artifacts/FlowYieldVaultsRequests.json${NC}" +jq '.abi' solidity/out/FlowYieldVaultsRequests.sol/FlowYieldVaultsRequests.json > "$CANONICAL_ABI_PATH" +cp "$CANONICAL_ABI_PATH" "$SOLIDITY_ABI_PATH" + +echo -e "${GREEN}✓ ABI exported to ${CANONICAL_ABI_PATH}${NC}" +echo -e "${GREEN}✓ ABI synced to ${SOLIDITY_ABI_PATH}${NC}" # Update addresses if network is specified if [[ -n "$NETWORK" ]]; then @@ -131,7 +136,8 @@ echo "" echo -e "${GREEN}Done!${NC}" echo "" echo "Exported files:" -echo " - deployments/artifacts/FlowYieldVaultsRequests.json (ABI)" +echo " - ${CANONICAL_ABI_PATH} (ABI)" +echo " - ${SOLIDITY_ABI_PATH} (ABI copy)" if [[ -n "$NETWORK" ]]; then echo " - deployments/contract-addresses.json (updated for ${NETWORK})" fi diff --git a/solidity/deployments/artifacts/FlowYieldVaultsRequests.json b/solidity/deployments/artifacts/FlowYieldVaultsRequests.json index 88f282c..32fc49f 100644 --- a/solidity/deployments/artifacts/FlowYieldVaultsRequests.json +++ b/solidity/deployments/artifacts/FlowYieldVaultsRequests.json @@ -19,6 +19,19 @@ "type": "receive", "stateMutability": "payable" }, + { + "type": "function", + "name": "DEFAULT_MINIMUM_BALANCE", + "inputs": [], + "outputs": [ + { + "name": "", + "type": "uint256", + "internalType": "uint256" + } + ], + "stateMutability": "view" + }, { "type": "function", "name": "NATIVE_FLOW", @@ -532,14 +545,19 @@ "internalType": "string[]" }, { - "name": "pendingBalance", - "type": "uint256", - "internalType": "uint256" + "name": "balanceTokens", + "type": "address[]", + "internalType": "address[]" }, { - "name": "claimableRefund", - "type": "uint256", - "internalType": "uint256" + "name": "pendingBalances", + "type": "uint256[]", + "internalType": "uint256[]" + }, + { + "name": "claimableRefundsArray", + "type": "uint256[]", + "internalType": "uint256[]" } ], "stateMutability": "view" @@ -1947,6 +1965,11 @@ "name": "CannotAllowlistZeroAddress", "inputs": [] }, + { + "type": "error", + "name": "CannotBlocklistZeroAddress", + "inputs": [] + }, { "type": "error", "name": "CannotRegisterSentinelYieldVaultId", @@ -1998,6 +2021,11 @@ "name": "InvalidCOAAddress", "inputs": [] }, + { + "type": "error", + "name": "InvalidMinimumBalance", + "inputs": [] + }, { "type": "error", "name": "InvalidRequestState", diff --git a/solidity/src/FlowYieldVaultsRequests.sol b/solidity/src/FlowYieldVaultsRequests.sol index 9e7982d..782ea9c 100644 --- a/solidity/src/FlowYieldVaultsRequests.sol +++ b/solidity/src/FlowYieldVaultsRequests.sol @@ -134,6 +134,12 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @notice Token configurations indexed by token address mapping(address => TokenConfig) public allowedTokens; + /// @notice Token addresses surfaced in per-user balance views + address[] private _trackedTokens; + + /// @notice O(1) lookup for tracked-token membership + mapping(address => bool) private _isTrackedToken; + /// @notice Maximum number of pending requests allowed per user (0 = unlimited) uint256 public maxPendingRequestsPerUser; @@ -1318,8 +1324,9 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { /// @return messages Messages /// @return vaultIdentifiers Vault identifiers /// @return strategyIdentifiers Strategy identifiers - /// @return pendingBalance Escrowed balance for active pending requests (native FLOW only) - /// @return claimableRefund Claimable refund amount (native FLOW only) + /// @return balanceTokens Token addresses for balance arrays + /// @return pendingBalances Escrowed balances for active pending requests per token + /// @return claimableRefundsArray Claimable refund amounts per token function getPendingRequestsByUserUnpacked( address user ) @@ -1336,8 +1343,9 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { string[] memory messages, string[] memory vaultIdentifiers, string[] memory strategyIdentifiers, - uint256 pendingBalance, - uint256 claimableRefund + address[] memory balanceTokens, + uint256[] memory pendingBalances, + uint256[] memory claimableRefundsArray ) { // Use the user's pending request IDs directly (O(1) lookup) @@ -1374,31 +1382,28 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { } } - // Get balances for native FLOW - pendingBalance = pendingUserBalances[user][NATIVE_FLOW]; - claimableRefund = claimableRefunds[user][NATIVE_FLOW]; + // Return balances for the full tracked token set so previously configured + // tokens remain visible even if later disabled. + uint256 tokenCount = _trackedTokens.length; + balanceTokens = new address[](tokenCount); + pendingBalances = new uint256[](tokenCount); + claimableRefundsArray = new uint256[](tokenCount); + + for (uint256 i = 0; i < tokenCount; ) { + address token = _trackedTokens[i]; + balanceTokens[i] = token; + pendingBalances[i] = pendingUserBalances[user][token]; + claimableRefundsArray[i] = claimableRefunds[user][token]; + unchecked { + ++i; + } + } } // ============================================ // Internal Functions // ============================================ - /// @dev Internal token configuration helper with minimum-balance validation. - function _setTokenConfig( - address tokenAddress, - bool isSupported, - uint256 minimumBalance, - bool isNative - ) internal { - if (isSupported && minimumBalance == 0) revert InvalidMinimumBalance(); - - allowedTokens[tokenAddress] = TokenConfig({ - isSupported: isSupported, - minimumBalance: minimumBalance, - isNative: isNative - }); - } - /** * @dev Internal implementation for dropping pending requests. * Silently skips requests that don't exist or aren't in PENDING status. @@ -1494,6 +1499,27 @@ contract FlowYieldVaultsRequests is ReentrancyGuard, Ownable2Step { } } + /// @dev Stores token configuration and records tokens for future balance queries. + function _setTokenConfig( + address tokenAddress, + bool isSupported, + uint256 minimumBalance, + bool isNative + ) internal { + if (isSupported && minimumBalance == 0) revert InvalidMinimumBalance(); + + allowedTokens[tokenAddress] = TokenConfig({ + isSupported: isSupported, + minimumBalance: minimumBalance, + isNative: isNative + }); + + if (isSupported && !_isTrackedToken[tokenAddress]) { + _isTrackedToken[tokenAddress] = true; + _trackedTokens.push(tokenAddress); + } + } + /** * @dev Internal implementation for starting request processing. * Transitions request to PROCESSING status and handles fund transfers. diff --git a/solidity/src/test/PendingRequestsByUserQueryMock.sol b/solidity/src/test/PendingRequestsByUserQueryMock.sol new file mode 100644 index 0000000..432c426 --- /dev/null +++ b/solidity/src/test/PendingRequestsByUserQueryMock.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/// @notice Tiny ERC20-like mock exposing only decimals(). +contract ERC20DecimalsOnlyMock { + uint8 internal immutable tokenDecimals; + + constructor(uint8 tokenDecimals_) { + tokenDecimals = tokenDecimals_; + } + + function decimals() external view returns (uint8) { + return tokenDecimals; + } +} + +/// @notice Test-only EVM mock for Cadence query coverage. +/// @dev Mirrors the production getPendingRequestsByUserUnpacked ABI exactly, +/// but returns synthetic fixed data instead of reading real request state. +contract PendingRequestsByUserQueryMock { + address internal constant TARGET_USER = 0x0000000000000000000000000000000000000011; + address internal constant NATIVE_FLOW = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; + address internal immutable tokenB; + address internal immutable tokenC; + + constructor(address tokenB_, address tokenC_) { + tokenB = tokenB_; + tokenC = tokenC_; + } + + function getPendingRequestsByUserUnpacked( + address user + ) + external + view + returns ( + uint256[] memory ids, + uint8[] memory requestTypes, + uint8[] memory statuses, + address[] memory tokenAddresses, + uint256[] memory amounts, + uint64[] memory yieldVaultIds, + uint256[] memory timestamps, + string[] memory messages, + string[] memory vaultIdentifiers, + string[] memory strategyIdentifiers, + address[] memory balanceTokens, + uint256[] memory pendingBalances, + uint256[] memory claimableRefundsArray + ) + { + balanceTokens = new address[](3); + pendingBalances = new uint256[](3); + claimableRefundsArray = new uint256[](3); + balanceTokens[0] = NATIVE_FLOW; + balanceTokens[1] = tokenB; + balanceTokens[2] = tokenC; + + if (user != TARGET_USER) { + ids = new uint256[](0); + requestTypes = new uint8[](0); + statuses = new uint8[](0); + tokenAddresses = new address[](0); + amounts = new uint256[](0); + yieldVaultIds = new uint64[](0); + timestamps = new uint256[](0); + messages = new string[](0); + vaultIdentifiers = new string[](0); + strategyIdentifiers = new string[](0); + return ( + ids, + requestTypes, + statuses, + tokenAddresses, + amounts, + yieldVaultIds, + timestamps, + messages, + vaultIdentifiers, + strategyIdentifiers, + balanceTokens, + pendingBalances, + claimableRefundsArray + ); + } + + ids = new uint256[](2); + requestTypes = new uint8[](2); + statuses = new uint8[](2); + tokenAddresses = new address[](2); + amounts = new uint256[](2); + yieldVaultIds = new uint64[](2); + timestamps = new uint256[](2); + messages = new string[](2); + vaultIdentifiers = new string[](2); + strategyIdentifiers = new string[](2); + + ids[0] = 11; + requestTypes[0] = 0; + statuses[0] = 0; + tokenAddresses[0] = NATIVE_FLOW; + amounts[0] = 1 ether; + yieldVaultIds[0] = type(uint64).max; + timestamps[0] = 100; + messages[0] = ""; + vaultIdentifiers[0] = "A.0ae53cb6e3f42a79.FlowToken.Vault"; + strategyIdentifiers[0] = "A.045a1763c93006ca.MockStrategies.TracerStrategy"; + + ids[1] = 12; + requestTypes[1] = 1; + statuses[1] = 0; + tokenAddresses[1] = tokenB; + amounts[1] = 1_234_567; + yieldVaultIds[1] = 42; + timestamps[1] = 101; + messages[1] = ""; + vaultIdentifiers[1] = ""; + strategyIdentifiers[1] = ""; + + pendingBalances[0] = 3 ether; + pendingBalances[1] = 1_234_567; + claimableRefundsArray[1] = 500_000; + } +} diff --git a/solidity/test/FlowYieldVaultsRequests.t.sol b/solidity/test/FlowYieldVaultsRequests.t.sol index cde4aa5..4fcd752 100644 --- a/solidity/test/FlowYieldVaultsRequests.t.sol +++ b/solidity/test/FlowYieldVaultsRequests.t.sol @@ -2,8 +2,8 @@ pragma solidity 0.8.20; import "forge-std/Test.sol"; +import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; import "../src/FlowYieldVaultsRequests.sol"; contract MockDAI is ERC20 { @@ -30,11 +30,14 @@ contract FlowYieldVaultsRequestsTestHelper is FlowYieldVaultsRequests { contract FlowYieldVaultsRequestsTest is Test { FlowYieldVaultsRequestsTestHelper public c; + ERC20Mock public wflow; + ERC20Mock public tokenB; address user = makeAddr("user"); address user2 = makeAddr("user2"); address coa = makeAddr("coa"); address constant NATIVE_FLOW = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF; - address constant WFLOW = 0xd3bF53DAC106A0290B0483EcBC89d40FcC961f3e; + address WFLOW; + address TOKEN_B; // Events for testing (from OpenZeppelin Ownable2Step) event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); @@ -57,6 +60,14 @@ contract FlowYieldVaultsRequestsTest is Test { function setUp() public { vm.deal(user, 100 ether); vm.deal(user2, 100 ether); + wflow = new ERC20Mock(); + tokenB = new ERC20Mock(); + WFLOW = address(wflow); + TOKEN_B = address(tokenB); + wflow.mint(user, 100 ether); + wflow.mint(user2, 100 ether); + tokenB.mint(user, 100 ether); + tokenB.mint(user2, 100 ether); c = new FlowYieldVaultsRequestsTestHelper(coa, WFLOW); c.testRegisterYieldVaultId(42, user, NATIVE_FLOW); @@ -1026,7 +1037,8 @@ contract FlowYieldVaultsRequestsTest is Test { string[] memory messages, string[] memory vaultIdentifiers, string[] memory strategyIdentifiers, - uint256 pendingBalance, + address[] memory balanceTokens, + uint256[] memory pendingBalances, ) = c.getPendingRequestsByUserUnpacked(user); assertEq(ids.length, 2, "User should have 2 pending requests"); @@ -1036,7 +1048,176 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(requestTypes[1], uint8(FlowYieldVaultsRequests.RequestType.DEPOSIT_TO_YIELDVAULT)); assertEq(amounts[0], 1 ether); assertEq(amounts[1], 2 ether); - assertEq(pendingBalance, 3 ether, "Pending balance should be sum of amounts"); + assertEq(balanceTokens[0], NATIVE_FLOW, "First balance token should be NATIVE_FLOW"); + assertEq(pendingBalances[0], 3 ether, "Pending balance should be sum of amounts"); + } + + function test_GetPendingRequestsByUserUnpacked_WFLOWBalances() public { + vm.startPrank(user); + wflow.approve(address(c), 2 ether); + + uint256 reqId = c.createYieldVault(WFLOW, 2 ether, VAULT_ID, STRATEGY_ID); + + ( + uint256[] memory ids, + , + , + , + , + , + , + , + , + , + address[] memory balanceTokens, + uint256[] memory pendingBalances, + uint256[] memory claimableRefundsArr + ) = c.getPendingRequestsByUserUnpacked(user); + + assertEq(ids.length, 1, "User should have 1 pending request"); + assertEq(ids[0], reqId, "Pending request should be the WFLOW create request"); + assertEq(balanceTokens.length, 2, "Should return NATIVE_FLOW and WFLOW balances"); + assertEq(balanceTokens[0], NATIVE_FLOW, "First balance token should be NATIVE_FLOW"); + assertEq(balanceTokens[1], WFLOW, "Second balance token should be WFLOW"); + assertEq(pendingBalances[0], 0, "NATIVE_FLOW pending balance should be 0"); + assertEq(pendingBalances[1], 2 ether, "WFLOW pending balance should match request amount"); + assertEq(claimableRefundsArr[0], 0, "NATIVE_FLOW refund balance should be 0"); + assertEq(claimableRefundsArr[1], 0, "WFLOW refund balance should be 0 before cancellation"); + assertEq(c.getUserPendingBalance(user, WFLOW), pendingBalances[1], "Direct WFLOW getter should match unpacked view"); + assertEq(c.getClaimableRefund(user, WFLOW), claimableRefundsArr[1], "Direct WFLOW refund getter should match unpacked view"); + + c.cancelRequest(reqId); + + ( + uint256[] memory idsAfterCancel, + , + , + , + , + , + , + , + , + , + address[] memory balanceTokensAfterCancel, + uint256[] memory pendingBalancesAfterCancel, + uint256[] memory claimableRefundsAfterCancel + ) = c.getPendingRequestsByUserUnpacked(user); + + assertEq(idsAfterCancel.length, 0, "Cancelled WFLOW request should no longer be pending"); + assertEq(balanceTokensAfterCancel.length, 2, "Token balance slots should remain stable"); + assertEq(balanceTokensAfterCancel[1], WFLOW, "Second balance token should remain WFLOW"); + assertEq(pendingBalancesAfterCancel[1], 0, "WFLOW pending balance should be cleared after cancellation"); + assertEq(claimableRefundsAfterCancel[1], 2 ether, "WFLOW refund balance should match cancelled amount"); + assertEq(c.getUserPendingBalance(user, WFLOW), 0, "Direct WFLOW pending getter should be cleared after cancellation"); + assertEq(c.getClaimableRefund(user, WFLOW), 2 ether, "Direct WFLOW refund getter should match cancelled amount"); + vm.stopPrank(); + } + + function test_GetPendingRequestsByUserUnpacked_IncludesTrackedERC20Balances() public { + vm.prank(c.owner()); + c.setTokenConfig(TOKEN_B, true, 0.5 ether, false); + + vm.startPrank(user); + tokenB.approve(address(c), 3 ether); + uint256 reqId = c.createYieldVault(TOKEN_B, 3 ether, VAULT_ID, STRATEGY_ID); + vm.stopPrank(); + + ( + uint256[] memory ids, + , + , + , + , + , + , + , + , + , + address[] memory balanceTokens, + uint256[] memory pendingBalances, + uint256[] memory claimableRefundsArr + ) = c.getPendingRequestsByUserUnpacked(user); + + assertEq(ids.length, 1, "User should have 1 pending request"); + assertEq(ids[0], reqId, "Pending request should be the TokenB create request"); + assertEq(balanceTokens.length, 3, "Should return all tracked token balances"); + assertEq(balanceTokens[0], NATIVE_FLOW, "First balance token should be NATIVE_FLOW"); + assertEq(balanceTokens[1], WFLOW, "Second balance token should be WFLOW"); + assertEq(balanceTokens[2], TOKEN_B, "Third balance token should be TokenB"); + assertEq(pendingBalances[0], 0, "NATIVE_FLOW pending balance should be 0"); + assertEq(pendingBalances[1], 0, "WFLOW pending balance should be 0"); + assertEq(pendingBalances[2], 3 ether, "TokenB pending balance should match request amount"); + assertEq(claimableRefundsArr[2], 0, "TokenB refund balance should be 0 before cancellation"); + assertEq(c.getUserPendingBalance(user, TOKEN_B), pendingBalances[2], "Direct TokenB getter should match unpacked view"); + assertEq(c.getClaimableRefund(user, TOKEN_B), claimableRefundsArr[2], "Direct TokenB refund getter should match unpacked view"); + } + + function test_GetPendingRequestsByUserUnpacked_KeepsTrackedTokenVisibleAfterDisable() public { + vm.prank(c.owner()); + c.setTokenConfig(TOKEN_B, true, 0.5 ether, false); + + vm.startPrank(user); + tokenB.approve(address(c), 2 ether); + uint256 reqId = c.createYieldVault(TOKEN_B, 2 ether, VAULT_ID, STRATEGY_ID); + c.cancelRequest(reqId); + vm.stopPrank(); + + vm.prank(c.owner()); + c.setTokenConfig(TOKEN_B, false, 0, false); + + ( + uint256[] memory ids, + , + , + , + , + , + , + , + , + , + address[] memory balanceTokens, + uint256[] memory pendingBalances, + uint256[] memory claimableRefundsArr + ) = c.getPendingRequestsByUserUnpacked(user); + + assertEq(ids.length, 0, "Cancelled TokenB request should no longer be pending"); + assertEq(balanceTokens.length, 3, "Tracked token slots should remain stable after disable"); + assertEq(balanceTokens[2], TOKEN_B, "Disabled tracked token should remain visible"); + assertEq(pendingBalances[2], 0, "Disabled tracked token should have no pending balance after cancellation"); + assertEq(claimableRefundsArr[2], 2 ether, "Disabled tracked token refund should remain visible"); + assertEq(c.getClaimableRefund(user, TOKEN_B), 2 ether, "Direct TokenB refund getter should match unpacked view"); + } + + function test_GetPendingRequestsByUserUnpacked_DoesNotTrackNeverSupportedToken() public { + vm.prank(c.owner()); + c.setTokenConfig(TOKEN_B, false, 0, false); + + ( + uint256[] memory ids, + , + , + , + , + , + , + , + , + , + address[] memory balanceTokens, + uint256[] memory pendingBalances, + uint256[] memory claimableRefundsArr + ) = c.getPendingRequestsByUserUnpacked(user); + + assertEq(ids.length, 0, "User should have no pending requests"); + assertEq(balanceTokens.length, 2, "Never-supported token should not be tracked"); + assertEq(balanceTokens[0], NATIVE_FLOW, "First balance token should be NATIVE_FLOW"); + assertEq(balanceTokens[1], WFLOW, "Second balance token should be WFLOW"); + assertEq(pendingBalances[0], 0, "NATIVE_FLOW pending balance should remain 0"); + assertEq(pendingBalances[1], 0, "WFLOW pending balance should remain 0"); + assertEq(claimableRefundsArr[0], 0, "NATIVE_FLOW refund balance should remain 0"); + assertEq(claimableRefundsArr[1], 0, "WFLOW refund balance should remain 0"); } function test_GetPendingRequestsByUserUnpacked_MultipleUsers() public { @@ -1050,23 +1231,23 @@ contract FlowYieldVaultsRequestsTest is Test { c.createYieldVault{value: 3 ether}(NATIVE_FLOW, 3 ether, VAULT_ID, STRATEGY_ID); // Check user's requests - (uint256[] memory userIds, , , , , , , , , , uint256 userBalance, ) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory userIds, , , , , , , , , , , uint256[] memory userBalances, ) = c.getPendingRequestsByUserUnpacked(user); assertEq(userIds.length, 2, "User should have 2 requests"); - assertEq(userBalance, 4 ether, "User pending balance should be 4 ether"); + assertEq(userBalances[0], 4 ether, "User pending balance should be 4 ether"); // Check user2's requests - (uint256[] memory user2Ids, , , , , , , , , , uint256 user2Balance, ) = c.getPendingRequestsByUserUnpacked(user2); + (uint256[] memory user2Ids, , , , , , , , , , , uint256[] memory user2Balances, ) = c.getPendingRequestsByUserUnpacked(user2); assertEq(user2Ids.length, 1, "User2 should have 1 request"); - assertEq(user2Balance, 2 ether, "User2 pending balance should be 2 ether"); + assertEq(user2Balances[0], 2 ether, "User2 pending balance should be 2 ether"); } function test_GetPendingRequestsByUserUnpacked_EmptyForNewUser() public { address newUser = makeAddr("newUser"); - (uint256[] memory ids, , , , , , , , , , uint256 pendingBalance, ) = c.getPendingRequestsByUserUnpacked(newUser); + (uint256[] memory ids, , , , , , , , , , , uint256[] memory pendingBalances, ) = c.getPendingRequestsByUserUnpacked(newUser); assertEq(ids.length, 0, "New user should have no pending requests"); - assertEq(pendingBalance, 0, "New user should have 0 pending balance"); + assertEq(pendingBalances[0], 0, "New user should have 0 pending balance"); } function test_GetPendingRequestsByUserUnpacked_AfterProcessing() public { @@ -1083,13 +1264,13 @@ contract FlowYieldVaultsRequestsTest is Test { vm.stopPrank(); // User should now have req1 and req3 - (uint256[] memory ids, , , , uint256[] memory amounts, , , , , , uint256 pendingBalance, ) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory ids, , , , uint256[] memory amounts, , , , , , , uint256[] memory pendingBalances, ) = c.getPendingRequestsByUserUnpacked(user); assertEq(ids.length, 2, "User should have 2 remaining requests"); // Note: Order in user array may change due to swap-and-pop optimization assertTrue(ids[0] == req1 || ids[0] == req3, "Should contain req1 or req3"); assertTrue(ids[1] == req1 || ids[1] == req3, "Should contain req1 or req3"); assertTrue(ids[0] != ids[1], "Should be different requests"); - assertEq(pendingBalance, 4 ether, "Pending balance should be 4 ether"); + assertEq(pendingBalances[0], 4 ether, "Pending balance should be 4 ether"); } function test_GetPendingRequestsByUserUnpacked_AfterCancel() public { @@ -1101,13 +1282,13 @@ contract FlowYieldVaultsRequestsTest is Test { c.cancelRequest(req1); vm.stopPrank(); - (uint256[] memory ids, , , , , , , , , , uint256 pendingBalance, uint256 claimableRefund) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory ids, , , , , , , , , , , uint256[] memory pendingBalances, uint256[] memory claimableRefundsArr) = c.getPendingRequestsByUserUnpacked(user); assertEq(ids.length, 1, "User should have 1 remaining request"); assertEq(ids[0], req2, "Remaining request should be req2"); // pendingBalance = 2 ether (escrowed for req2 only) // claimableRefund = 1 ether (from cancelled req1) - assertEq(pendingBalance, 2 ether); - assertEq(claimableRefund, 1 ether); + assertEq(pendingBalances[0], 2 ether); + assertEq(claimableRefundsArr[0], 1 ether); } // ============================================ @@ -1141,16 +1322,16 @@ contract FlowYieldVaultsRequestsTest is Test { vm.stopPrank(); // Verify user1's remaining requests - (uint256[] memory u1Ids, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory u1Ids, , , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user); assertEq(u1Ids.length, 2, "User1 should have 2 requests"); // Verify user2's requests unchanged - (uint256[] memory u2Ids, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user2); + (uint256[] memory u2Ids, , , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user2); assertEq(u2Ids.length, 1, "User2 should have 1 request"); assertEq(u2Ids[0], u2r1); // Verify user3's requests unchanged - (uint256[] memory u3Ids, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user3); + (uint256[] memory u3Ids, , , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user3); assertEq(u3Ids.length, 1, "User3 should have 1 request"); assertEq(u3Ids[0], u3r1); } @@ -1168,9 +1349,9 @@ contract FlowYieldVaultsRequestsTest is Test { c.completeProcessing(req2, true, 101, "Created"); vm.stopPrank(); - (uint256[] memory ids, , , , , , , , , , uint256 pendingBalance, ) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory ids, , , , , , , , , , , uint256[] memory pendingBalances, ) = c.getPendingRequestsByUserUnpacked(user); assertEq(ids.length, 0, "User should have no pending requests"); - assertEq(pendingBalance, 0); + assertEq(pendingBalances[0], 0); } // ============================================ @@ -1332,7 +1513,7 @@ contract FlowYieldVaultsRequestsTest is Test { // Verify all other users still have 3 requests for (uint256 i = 0; i < 5; i++) { - (uint256[] memory ids, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(users[i]); + (uint256[] memory ids, , , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(users[i]); if (i == 2) { assertEq(ids.length, 2, "User 2 should have 2 requests"); } else { @@ -1356,7 +1537,7 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(c.getPendingRequestCount(), 0); - (uint256[] memory ids, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory ids, , , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user); assertEq(ids.length, 0); } @@ -1374,13 +1555,13 @@ contract FlowYieldVaultsRequestsTest is Test { vm.stopPrank(); // req1 should still be removed from pending (it's marked FAILED) - (uint256[] memory ids, , , , , , , , , , uint256 pendingBalance, uint256 claimableRefund) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory ids, , , , , , , , , , , uint256[] memory pendingBalances, uint256[] memory claimableRefundsArr) = c.getPendingRequestsByUserUnpacked(user); assertEq(ids.length, 1, "Should have 1 pending request"); assertEq(ids[0], req2); // Escrowed balance only includes req2 - assertEq(pendingBalance, 2 ether, "Escrowed balance should be req2 amount"); + assertEq(pendingBalances[0], 2 ether, "Escrowed balance should be req2 amount"); // Refunded amount is in claimableRefunds - assertEq(claimableRefund, 1 ether, "Claimable refund should be req1 amount"); + assertEq(claimableRefundsArr[0], 1 ether, "Claimable refund should be req1 amount"); } function test_CancelMiddleRequest_UpdatesIndexCorrectly() public { @@ -1400,7 +1581,7 @@ contract FlowYieldVaultsRequestsTest is Test { assertEq(globalIds[1], req3, "Second should be req3"); // Verify user's array - (uint256[] memory userIds, , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user); + (uint256[] memory userIds, , , , , , , , , , , , ) = c.getPendingRequestsByUserUnpacked(user); assertEq(userIds.length, 2); }