diff --git a/FlowActions b/FlowActions index 6769d4c9..f24bb66e 160000 --- a/FlowActions +++ b/FlowActions @@ -1 +1 @@ -Subproject commit 6769d4c9f9ded4a5b4404d8c982300e84ccef532 +Subproject commit f24bb66e440224921d8e521b596790d01a0b4580 diff --git a/README.md b/README.md index 4714564b..d37f933a 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ FlowALP/ - `FungibleToken.Vault`: Standard token operations - `DeFiActions.Sink/Source`: DeFi protocol composability -- Entitlements: `FlowALPv0.EParticipant`, `FlowALPv0.EPosition`, `FlowALPv0.EGovernance`, `FlowALPv0.ERebalance` +- Entitlements: `FlowALPModels.EParticipant`, `FlowALPModels.EPosition`, `FlowALPModels.EGovernance`, `FlowALPModels.ERebalance` ## πŸ› οΈ Development diff --git a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc index 3ca84da8..f3b8c7ed 100644 --- a/cadence/tests/adversarial_recursive_withdraw_source_test.cdc +++ b/cadence/tests/adversarial_recursive_withdraw_source_test.cdc @@ -120,7 +120,7 @@ fun testRecursiveWithdrawSource() { // In this test, the topUpSource behavior is adversarial: it attempts to re-enter // the pool during the pull/deposit flow. We expect the transaction to fail. let withdrawRes = executeTransaction( - "./transactions/flow-alp/pool-management/withdraw_from_position.cdc", + "./transactions/flow-alp/epositionadmin/withdraw_from_position.cdc", [positionID, flowTokenIdentifier, 1500.0, true], // pullFromTopUpSource: true userAccount ) diff --git a/cadence/tests/adversarial_type_spoofing_test.cdc b/cadence/tests/adversarial_type_spoofing_test.cdc index 1edb2614..2d793af8 100644 --- a/cadence/tests/adversarial_type_spoofing_test.cdc +++ b/cadence/tests/adversarial_type_spoofing_test.cdc @@ -59,7 +59,7 @@ fun testMaliciousSource() { // withdraw 1337 Flow from the position let withdrawRes = executeTransaction( - "./transactions/flow-alp/pool-management/withdraw_from_position.cdc", + "./transactions/flow-alp/epositionadmin/withdraw_from_position.cdc", [1 as UInt64, flowTokenIdentifier, 1337.0, true], hackerAccount ) diff --git a/cadence/tests/async_update_position_test.cdc b/cadence/tests/async_update_position_test.cdc index f6dc85ae..a7001255 100644 --- a/cadence/tests/async_update_position_test.cdc +++ b/cadence/tests/async_update_position_test.cdc @@ -41,7 +41,7 @@ fun testUpdatePosition() { depositToPosition(signer: user, positionID: 0, amount: 600.0, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) let updatePositionRes = _executeTransaction( - "./transactions/flow-alp/pool-management/async_update_position.cdc", + "./transactions/flow-alp/eimplementation/async_update_position.cdc", [ 0 as UInt64 ], PROTOCOL_ACCOUNT ) diff --git a/cadence/tests/cap_test.cdc b/cadence/tests/cap_test.cdc index d2df3573..d6d59f2e 100644 --- a/cadence/tests/cap_test.cdc +++ b/cadence/tests/cap_test.cdc @@ -4,78 +4,824 @@ import BlockchainHelpers import "MOET" import "test_helpers.cdc" -// ----------------------------------------------------------------------------- -// Pool Creation Workflow Test -// ----------------------------------------------------------------------------- -// Validates that a pool can be created and that essential invariants hold. -// ----------------------------------------------------------------------------- +// ============================================================================= +// Security Permission Tests +// ============================================================================= +// +// This file validates the Actor Capability Matrix defined in: +// docs/security-permission-matrix.md +// +// One section per entitlement. Each section uses the production actor for that +// entitlement and covers ALL matrix operations for that entitlement. +// Role accounts are configured in setup() with the exact access they would +// receive in production. +// +// eParticipantUser β€” Capability +// Published via the FIXED publish_beta_cap.cdc. +// Cap stored at FlowALPv0.PoolCapStoragePath. +// +// ePositionUser β€” Capability +// EPosition-only capability; can perform pool-level position +// ops on any position by ID. No EParticipant. +// Cap stored at FlowALPv0.PoolCapStoragePath. +// +// eParticipantPositionUser β€” Capability over-grant +// Current (unfixed) beta cap β€” grants EPosition unnecessarily. +// Cap stored at FlowALPv0.PoolCapStoragePath. +// +// eRebalanceUser β€” Capability +// Narrowly-scoped cap for rebalancer contracts. +// Cap stored at FlowALPv0.PoolCapStoragePath. +// +// ePositionAdminUser β€” No Pool capability; has PositionManager in own storage. +// EPositionAdmin access is via storage ownership β€” cannot +// be delegated as a capability. +// +// eGovernanceUser β€” Capability +// Granted by PROTOCOL_ACCOUNT via grant_egovernance_cap.cdc. +// Cap stored at FlowALPv0.PoolCapStoragePath. +// +// PROTOCOL_ACCOUNT β€” Pool owner; exercises EImplementation directly via storage borrow. +// +// Negative tests: +// Cadence entitlements for Pool capabilities are enforced by the Cadence type checker. +// Only borrowAuthorizedPosition has a runtime enforcement (it panics if the pid is not in +// the signer's PositionManager), so testEPositionAdmin_BorrowUnauthorizedPosition_Fails tests that path. +// ============================================================================= + + +// Position created for PROTOCOL_ACCOUNT in setup β€” used as target for EPosition tests. +access(all) var setupPid: UInt64 = 0 +access(all) var ePositionAdminPid: UInt64 = 0 access(all) var snapshot: UInt64 = 0 +// Role accounts +access(all) var eParticipantUser = Test.createAccount() +access(all) var ePositionUser = Test.createAccount() +access(all) var eParticipantPositionUser = Test.createAccount() +access(all) var eRebalanceUser = Test.createAccount() +access(all) var ePositionAdminUser = Test.createAccount() +access(all) var eGovernanceUser = Test.createAccount() + +access(all) +fun safeReset() { + if getCurrentBlockHeight() > snapshot { + Test.reset(to: snapshot) + } +} + +/// Execute a 2-authorizer transaction (e.g. admin + grantee for capability setup). +access(all) +fun _execute2Signers( + _ path: String, + _ args: [AnyStruct], + _ s1: Test.TestAccount, + _ s2: Test.TestAccount +): Test.TransactionResult { + let signers = s1.address == s2.address ? [s1] : [s1, s2] + return Test.executeTransaction(Test.Transaction( + code: Test.readFile(path), + authorizers: [s1.address, s2.address], + signers: signers, + arguments: args + )) +} + // ----------------------------------------------------------------------------- // SETUP // ----------------------------------------------------------------------------- + access(all) fun setup() { deployContracts() + // Create pool with MOET as the default token, oracle price = $1 createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) + setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: 1.0) + mintMoet(signer: PROTOCOL_ACCOUNT, to: PROTOCOL_ACCOUNT.address, amount: 1_000.0, beFailed: false) + // Verify pool invariants before setting up role accounts let exists = poolExists(address: PROTOCOL_ACCOUNT.address) Test.assert(exists) - - // Reserve balance should be zero for default token let reserveBal = getReserveBalance(vaultIdentifier: MOET_TOKEN_IDENTIFIER) Test.assertEqual(0.0, reserveBal) + // Create setupPid=0 owned by PROTOCOL_ACCOUNT. + // Used as target in EPosition/ERebalance tests. + createPosition( + admin: PROTOCOL_ACCOUNT, + signer: PROTOCOL_ACCOUNT, + amount: 10.0, + vaultStoragePath: MOET.VaultStoragePath, + pushToDrawDownSink: false + ) + + setupPid = getLastPositionId() + + // ───────────────────────────────────────────────────────────────────────── + // EParticipant user β€” EParticipant-ONLY capability (fixed beta cap) + // ───────────────────────────────────────────────────────────────────────── + setupMoetVault(eParticipantUser, beFailed: false) + mintMoet(signer: PROTOCOL_ACCOUNT, to: eParticipantUser.address, amount: 100.0, beFailed: false) + Test.expect( + _execute2Signers( + "../tests/transactions/flow-alp/setup/grant_eparticipant_cap.cdc", + [], + PROTOCOL_ACCOUNT, + eParticipantUser + ), + Test.beSucceeded() + ) + + // ───────────────────────────────────────────────────────────────────────── + // EPosition user β€” EPosition-ONLY capability (no EParticipant) + // ───────────────────────────────────────────────────────────────────────── + setupMoetVault(ePositionUser, beFailed: false) + mintMoet(signer: PROTOCOL_ACCOUNT, to: ePositionUser.address, amount: 100.0, beFailed: false) + Test.expect( + _execute2Signers( + "../tests/transactions/flow-alp/setup/grant_eposition_cap.cdc", + [], + PROTOCOL_ACCOUNT, + ePositionUser + ), + Test.beSucceeded() + ) + + // ───────────────────────────────────────────────────────────────────────── + // EParticipantPosition user β€” EParticipant+EPosition capability (current over-grant) + // ───────────────────────────────────────────────────────────────────────── + setupMoetVault(eParticipantPositionUser, beFailed: false) + mintMoet(signer: PROTOCOL_ACCOUNT, to: eParticipantPositionUser.address, amount: 100.0, beFailed: false) + grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, eParticipantPositionUser) + + // ───────────────────────────────────────────────────────────────────────── + // ERebalance user β€” ERebalance-only capability (rebalancer simulation) + // ───────────────────────────────────────────────────────────────────────── + Test.expect( + _execute2Signers( + "../tests/transactions/flow-alp/setup/grant_erebalance_cap.cdc", + [], + PROTOCOL_ACCOUNT, + eRebalanceUser + ), + Test.beSucceeded() + ) + + // ───────────────────────────────────────────────────────────────────────── + // EPositionAdmin user β€” has PositionManager in own storage (pid=1) + // EPositionAdmin access comes from storage ownership, not a delegated cap. + // ───────────────────────────────────────────────────────────────────────── + setupMoetVault(ePositionAdminUser, beFailed: false) + mintMoet(signer: PROTOCOL_ACCOUNT, to: ePositionAdminUser.address, amount: 100.0, beFailed: false) + createPosition( + admin: PROTOCOL_ACCOUNT, + signer: ePositionAdminUser, + amount: 10.0, + vaultStoragePath: MOET.VaultStoragePath, + pushToDrawDownSink: false + ) + ePositionAdminPid = getLastPositionId() + + // ───────────────────────────────────────────────────────────────────────── + // EGovernance user β€” EGovernance capability delegated from PROTOCOL_ACCOUNT + // ───────────────────────────────────────────────────────────────────────── + Test.expect( + _execute2Signers( + "../tests/transactions/flow-alp/setup/grant_egovernance_cap.cdc", + [], + PROTOCOL_ACCOUNT, + eGovernanceUser + ), + Test.beSucceeded() + ) + snapshot = getCurrentBlockHeight() } -// ----------------------------------------------------------------------------- -// TEST CASES -// ----------------------------------------------------------------------------- +// ============================================================================= +// Publish / Claim flow β€” capability grant mechanism +// ============================================================================= +/// publish β†’ claim β†’ create position round-trip using production beta transactions. access(all) -fun testPositionCreationFail() { +fun testPublishClaimCap() { + safeReset() + + let publishCapResult = _executeTransaction( + "../transactions/flow-alp/beta/publish_beta_cap.cdc", + [PROTOCOL_ACCOUNT.address], + PROTOCOL_ACCOUNT + ) + Test.expect(publishCapResult, Test.beSucceeded()) - let txResult = _executeTransaction( - "../tests/transactions/flow-alp/pool-management/01_negative_no_eparticipant_fail.cdc", + let claimCapResult = _executeTransaction( + "../transactions/flow-alp/beta/claim_and_save_beta_cap.cdc", + [PROTOCOL_ACCOUNT.address], + PROTOCOL_ACCOUNT + ) + Test.expect(claimCapResult, Test.beSucceeded()) + + let createPositionResult = _executeTransaction( + "../tests/transactions/flow-alp/eparticipant/create_position_via_published_cap.cdc", [], PROTOCOL_ACCOUNT ) - Test.expect(txResult, Test.beFailed()) + Test.expect(createPositionResult, Test.beSucceeded()) +} + +// ============================================================================= +// EParticipant β€” fixed beta capability (EParticipant only) +// ============================================================================= +// +// Actor: eParticipantUser β€” Capability +// Matrix rows: createPosition, depositToPosition + +/// EParticipant cap allows createPosition and depositToPosition. +access(all) +fun testEParticipant_CreateAndDeposit() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eparticipant/create_and_deposit_via_cap.cdc", + [], + eParticipantUser + ) + Test.expect(result, Test.beSucceeded()) +} + +// ============================================================================= +// EParticipant+EPosition β€” over-grant (current beta cap via publish_beta_cap.cdc) +// ============================================================================= +// +// Actor: eParticipantPositionUser β€” Capability +// Issued by publish_beta_cap.cdc and stored at FlowALPv0.PoolCapStoragePath. +// This is the CURRENT (unfixed) beta cap. EPosition is NOT needed for normal +// user actions; its presence lets this actor perform pool-level position ops +// on ANY position, including positions owned by other accounts. +// +// Matrix rows: createPosition (EParticipant), depositToPosition (EParticipant), +// withdraw [OVERGRANT], withdrawAndPull [OVERGRANT], depositAndPush [OVERGRANT], +// lockPosition [OVERGRANT], unlockPosition [OVERGRANT], rebalancePosition [OVERGRANT], +// rebalance (Position) [OVERGRANT β€” same entry point as rebalancePosition] +// +// The [OVERGRANT] rows confirm the security issue: a normal beta user can operate on +// positions they do not own (setupPid is owned by PROTOCOL_ACCOUNT). + +/// Over-granted beta cap still allows EParticipant operations (createPosition, depositToPosition). +access(all) +fun testEParticipantPosition_CreateAndDeposit() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eparticipant/create_and_deposit_via_cap.cdc", + [], + eParticipantPositionUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// Over-granted beta cap allows Pool.withdraw on ANY position β€” including +/// setupPid owned by PROTOCOL_ACCOUNT. +access(all) +fun testEParticipantPosition_WithdrawAnyPosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eposition/withdraw_any.cdc", + [setupPid, 1.0], + eParticipantPositionUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// Over-granted beta cap allows Pool.withdrawAndPull on ANY position β€” including +/// positions owned by other accounts. +access(all) +fun testEParticipantPosition_WithdrawAndPullAnyPosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eposition/withdraw_and_pull_any.cdc", + [setupPid, 1.0], + eParticipantPositionUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// Over-granted beta cap allows Pool.depositAndPush on ANY position β€” including +/// positions owned by other accounts. +access(all) +fun testEParticipantPosition_DepositAndPushAnyPosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eposition/deposit_and_push_any.cdc", + [setupPid, 1.0], + eParticipantPositionUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// Over-granted beta cap allows Pool.lockPosition and Pool.unlockPosition on ANY position β€” +/// including positions owned by other accounts. +access(all) +fun testEParticipantPosition_LockUnlockAnyPosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eposition/lock_any.cdc", + [setupPid], + eParticipantPositionUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// Over-granted beta cap allows Pool.rebalancePosition on any position. +access(all) +fun testEParticipantPosition_RebalancePosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eposition/rebalance_pool.cdc", + [setupPid, true], + eParticipantPositionUser + ) + Test.expect(result, Test.beSucceeded()) +} + +// ============================================================================= +// EPosition β€” narrowly-scoped EPosition-only Pool capability +// ============================================================================= +// +// Actor: ePositionUser β€” Capability +// Matrix rows: withdraw, withdrawAndPull, depositAndPush, lockPosition, unlockPosition, +// rebalancePosition + +/// EPosition cap allows Pool.withdraw on ANY position by ID β€” including +/// setupPid owned by PROTOCOL_ACCOUNT. +access(all) +fun testEPosition_WithdrawAnyPosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eposition/withdraw_any.cdc", + [setupPid, 1.0], + ePositionUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EPosition cap allows Pool.withdrawAndPull on ANY position β€” including positions +/// owned by other accounts. +access(all) +fun testEPosition_WithdrawAndPullAnyPosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eposition/withdraw_and_pull_any.cdc", + [setupPid, 1.0], + ePositionUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EPosition cap allows Pool.depositAndPush on ANY position β€” including positions +/// owned by other accounts. +access(all) +fun testEPosition_DepositAndPushAnyPosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eposition/deposit_and_push_any.cdc", + [setupPid, 1.0], + ePositionUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EPosition cap allows Pool.lockPosition and Pool.unlockPosition on ANY position β€” +/// including positions owned by other accounts. +access(all) +fun testEPosition_LockUnlockAnyPosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eposition/lock_any.cdc", + [setupPid], + ePositionUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EPosition cap allows Pool.rebalancePosition. +access(all) +fun testEPosition_RebalancePosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eposition/rebalance_pool.cdc", + [setupPid, true], + ePositionUser + ) + Test.expect(result, Test.beSucceeded()) +} + +// ============================================================================= +// ERebalance β€” narrowly-scoped rebalancer capability +// ============================================================================= +// +// Actor: eRebalanceUser β€” Capability @ PoolCapStoragePath +// Matrix rows: rebalancePosition, rebalance (Position) +// Both tested via pool.rebalancePosition(); Position.rebalance() delegates to same call. +// Contract fix: Position.pool changed to Capability +// so the internal call chain works for ERebalance callers. + +/// ERebalance cap allows Pool.rebalancePosition. +access(all) +fun testERebalance_RebalancePosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/erebalance/rebalance_pool.cdc", + [setupPid, true], + eRebalanceUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// Matrix row: rebalance (Position) β€” Position.rebalance() delegates to Pool.rebalancePosition() +/// internally, so both matrix rows share the same Pool-level entry point. There is no separate +/// transaction that calls Position.rebalance() directly; this test confirms the ERebalance +/// entitlement is sufficient for the rebalancePosition call that Position.rebalance() invokes. +/// (The contract fix changes Position.pool to Capability +/// so the internal call chain accepts ERebalance callers.) +access(all) +fun testERebalance_PositionRebalance() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/erebalance/rebalance_pool.cdc", + [setupPid, true], + eRebalanceUser + ) + Test.expect(result, Test.beSucceeded()) +} + +// ============================================================================= +// EPositionAdmin β€” storage ownership of PositionManager (not a capability) +// ============================================================================= +// +// Actor: ePositionAdminUser β€” has PositionManager in own storage (cannot be delegated). +// Matrix rows: setTargetHealth, setMinHealth, setMaxHealth, provideSink, provideSource, +// addPosition (Manager), removePosition (Manager), borrowAuthorizedPosition +// +// Note: testEPositionAdmin_AddRemovePosition uses PROTOCOL_ACCOUNT because +// add_remove_position.cdc creates a fresh position via pool storage (EParticipant), +// which ePositionAdminUser does not hold. The EPositionAdmin entitlement is still +// tested via the PositionManager borrow inside the transaction. + +/// EPositionAdmin allows Position.setTargetHealth (via PositionManager.borrowAuthorizedPosition). +access(all) +fun testEPositionAdmin_SetTargetHealth() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/epositionadmin/set_target_health.cdc", + [ePositionAdminPid, TARGET_HEALTH], + ePositionAdminUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EPositionAdmin allows Position.setMinHealth (via PositionManager.borrowAuthorizedPosition). +access(all) +fun testEPositionAdmin_SetMinHealth() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/epositionadmin/set_min_health.cdc", + [ePositionAdminPid, MIN_HEALTH], + ePositionAdminUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EPositionAdmin allows Position.setMaxHealth (via PositionManager.borrowAuthorizedPosition). +access(all) +fun testEPositionAdmin_SetMaxHealth() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/epositionadmin/set_max_health.cdc", + [ePositionAdminPid, MAX_HEALTH], + ePositionAdminUser + ) + Test.expect(result, Test.beSucceeded()) } +/// EPositionAdmin allows Position.provideSink. +/// Sets a DummySink (accepts MOET) then clears it with nil. access(all) -fun testPositionCreationSuccess() { - Test.reset(to: snapshot) +fun testEPositionAdmin_ProvideSink() { + safeReset() - let txResult = _executeTransaction( - "../tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc", + let result = _executeTransaction( + "../tests/transactions/flow-alp/epositionadmin/provide_sink.cdc", + [ePositionAdminPid], + ePositionAdminUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EPositionAdmin allows Position.provideSource. +/// Calls provideSource(nil) to clear any existing source β€” always valid. +access(all) +fun testEPositionAdmin_ProvideSource() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/epositionadmin/provide_source.cdc", + [ePositionAdminPid], + ePositionAdminUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EPositionAdmin allows PositionManager.addPosition and PositionManager.removePosition. +/// Creates a fresh position, adds it to the manager, removes it, and destroys it. +/// Uses PROTOCOL_ACCOUNT because the transaction needs pool storage access to create +/// the position (EParticipant) β€” which ePositionAdminUser does not hold. +access(all) +fun testEPositionAdmin_AddRemovePosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/epositionadmin/add_remove_position.cdc", [], PROTOCOL_ACCOUNT ) + Test.expect(result, Test.beSucceeded()) +} + +/// EPositionAdmin allows PositionManager.borrowAuthorizedPosition. +access(all) +fun testEPositionAdmin_BorrowAuthorizedPosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/epositionadmin/borrow_authorized.cdc", + [ePositionAdminPid], + ePositionAdminUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// Negative: borrowAuthorizedPosition panics when the requested pid is not in the signer's +/// PositionManager. setupPid is owned by PROTOCOL_ACCOUNT, not ePositionAdminUser. +/// This is the only runtime-enforced access denial in this file β€” all other entitlements +/// are enforced statically by the Cadence type checker at check time. +access(all) +fun testEPositionAdmin_BorrowUnauthorizedPosition_Fails() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/epositionadmin/borrow_authorized.cdc", + [setupPid], + ePositionAdminUser + ) + Test.expect(result, Test.beFailed()) +} - Test.expect(txResult, Test.beSucceeded()) -} +// ============================================================================= +// EGovernance β€” capability-delegated governance access +// ============================================================================= +// +// Actor: eGovernanceUser β€” Capability +// Matrix rows: pausePool/unpausePool, addSupportedToken, setInterestCurve, setInsuranceRate, +// setStabilityFeeRate, setLiquidationParams, setPauseParams, setDepositLimitFraction, +// collectInsurance, collectStability, setDEX, setPriceOracle +// +// Note: withdrawStabilityFund (EGovernance) requires an active stability fund +// (non-zero debit balance + elapsed time + non-zero fee rate) and is therefore +// covered by the dedicated withdraw_stability_funds_test.cdc. +/// EGovernance cap allows Pool.pausePool and Pool.unpausePool. access(all) -fun testNegativeCap() { - Test.reset(to: snapshot) +fun testEGovernance_PauseUnpause() { + safeReset() - let negativeResult = _executeTransaction("../tests/transactions/flow-alp/pool-management/05_negative_cap.cdc", [], NON_ADMIN_ACCOUNT) - Test.expect(negativeResult, Test.beFailed()) + let pauseResult = _executeTransaction( + "../tests/transactions/flow-alp/egovernance/set_pool_paused.cdc", + [true], + eGovernanceUser + ) + Test.expect(pauseResult, Test.beSucceeded()) + + let unpauseResult = _executeTransaction( + "../tests/transactions/flow-alp/egovernance/set_pool_paused.cdc", + [false], + eGovernanceUser + ) + Test.expect(unpauseResult, Test.beSucceeded()) } +/// EGovernance cap allows Pool.addSupportedToken. +/// FlowToken is not added in setup, so this exercises a fresh token addition. access(all) -fun testPublishClaimCap() { - Test.reset(to: snapshot) - - let publishCapResult = _executeTransaction("../transactions/flow-alp/beta/publish_beta_cap.cdc", [PROTOCOL_ACCOUNT.address], PROTOCOL_ACCOUNT) - Test.expect(publishCapResult, Test.beSucceeded()) +fun testEGovernance_AddSupportedToken() { + safeReset() - let claimCapResult = _executeTransaction("../transactions/flow-alp/beta/claim_and_save_beta_cap.cdc", [PROTOCOL_ACCOUNT.address], PROTOCOL_ACCOUNT) - Test.expect(claimCapResult, Test.beSucceeded()) + // Oracle price for FlowToken is needed before using it as collateral/borrow, + // but adding a token to the pool does not require a price. + let result = _executeTransaction( + "../tests/transactions/flow-alp/egovernance/add_supported_token.cdc", + [FLOW_TOKEN_IDENTIFIER, 0.8, 0.8, 1_000_000.0, 1_000_000.0], + eGovernanceUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EGovernance cap allows Pool.setInterestCurve. +access(all) +fun testEGovernance_SetInterestCurve() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/egovernance/set_interest_curve.cdc", + [MOET_TOKEN_IDENTIFIER, 0.05 as UFix128], + eGovernanceUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EGovernance cap allows Pool.setInsuranceRate. +/// Uses rate=0.0 because a non-zero rate requires an insurance swapper to be configured; +/// a zero rate still exercises the EGovernance entitlement check without that prerequisite. +access(all) +fun testEGovernance_SetInsuranceRate() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/egovernance/set_insurance_rate.cdc", + [MOET_TOKEN_IDENTIFIER, 0.0], + eGovernanceUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EGovernance cap allows Pool.setStabilityFeeRate. +access(all) +fun testEGovernance_SetStabilityFeeRate() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/egovernance/set_stability_fee_rate.cdc", + [MOET_TOKEN_IDENTIFIER, 0.05], + eGovernanceUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EGovernance cap allows Pool.setLiquidationParams (via borrowConfig). +access(all) +fun testEGovernance_SetLiquidationParams() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/egovernance/set_liquidation_params.cdc", + [1.05 as UFix128], + eGovernanceUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EGovernance cap allows Pool.setPauseParams (via borrowConfig). +access(all) +fun testEGovernance_SetPauseParams() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/egovernance/set_pause_params.cdc", + [300 as UInt64], + eGovernanceUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EGovernance cap allows Pool.setDepositLimitFraction. +access(all) +fun testEGovernance_SetDepositLimitFraction() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/egovernance/set_deposit_limit_fraction.cdc", + [MOET_TOKEN_IDENTIFIER, 0.10], + eGovernanceUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EGovernance cap allows Pool.collectInsurance. +/// No insurance has accrued (zero insurance rate), but the call itself is valid. +access(all) +fun testEGovernance_CollectInsurance() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/egovernance/collect_insurance.cdc", + [MOET_TOKEN_IDENTIFIER], + eGovernanceUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EGovernance cap allows Pool.collectStability. +/// No stability fees have accrued (zero stability rate), but the call itself is valid. +access(all) +fun testEGovernance_CollectStability() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/egovernance/collect_stability.cdc", + [MOET_TOKEN_IDENTIFIER], + eGovernanceUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EGovernance cap allows Pool.setDEX (via borrowConfig). +/// Uses MockDexSwapper.SwapperProvider as the DEX implementation. +access(all) +fun testEGovernance_SetDEX() { + safeReset() - let createPositionResult = _executeTransaction("../tests/transactions/flow-alp/pool-management/04_create_position.cdc", [], PROTOCOL_ACCOUNT) + let result = _executeTransaction( + "../tests/transactions/flow-alp/egovernance/set_dex.cdc", + [], + eGovernanceUser + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EGovernance cap allows Pool.setPriceOracle. +/// Uses MockOracle.PriceOracle whose unitOfAccount matches the pool's default token (MOET). +access(all) +fun testEGovernance_SetPriceOracle() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/egovernance/set_oracle.cdc", + [], + eGovernanceUser + ) + Test.expect(result, Test.beSucceeded()) +} + +// ============================================================================= +// EImplementation β€” protocol internals (never issued externally) +// ============================================================================= +// +// Actor: PROTOCOL_ACCOUNT β€” pool owner; EImplementation via direct storage borrow. +// Matrix rows: asyncUpdate, asyncUpdatePosition, regenerateAllDepositCapacities + +/// EImplementation allows Pool.asyncUpdate. +/// The queue may be empty in tests; asyncUpdate is a no-op when the queue is empty. +access(all) +fun testEImplementation_AsyncUpdate() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eimplementation/async_update.cdc", + [], + PROTOCOL_ACCOUNT + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EImplementation allows Pool.asyncUpdatePosition. +access(all) +fun testEImplementation_AsyncUpdatePosition() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eimplementation/async_update_position.cdc", + [setupPid], + PROTOCOL_ACCOUNT + ) + Test.expect(result, Test.beSucceeded()) +} + +/// EImplementation allows Pool.regenerateAllDepositCapacities. +access(all) +fun testEImplementation_RegenerateAllDepositCapacities() { + safeReset() + + let result = _executeTransaction( + "../tests/transactions/flow-alp/eimplementation/regenerate_capacities.cdc", + [], + PROTOCOL_ACCOUNT + ) + Test.expect(result, Test.beSucceeded()) } diff --git a/cadence/tests/insurance_swapper_test.cdc b/cadence/tests/insurance_swapper_test.cdc index 8885b6a6..fdd70637 100644 --- a/cadence/tests/insurance_swapper_test.cdc +++ b/cadence/tests/insurance_swapper_test.cdc @@ -182,7 +182,7 @@ access(all) fun test_setInsuranceSwapper_wrongOutputType_fails() { // try to set a swapper that doesn't output MOET (outputs FLOW_TOKEN_IDENTIFIER instead) let res = _executeTransaction( - "./transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc", + "./transactions/flow-alp/egovernance/set_insurance_swapper_mock.cdc", [MOET_TOKEN_IDENTIFIER, 1.0, MOET_TOKEN_IDENTIFIER, FLOW_TOKEN_IDENTIFIER], PROTOCOL_ACCOUNT ) @@ -199,7 +199,7 @@ access(all) fun test_setInsuranceSwapper_wrongInputType_fails() { // try to set a swapper with wrong input type (FLOW_TOKEN_IDENTIFIER instead of MOET_TOKEN_IDENTIFIER) let res = _executeTransaction( - "./transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc", + "./transactions/flow-alp/egovernance/set_insurance_swapper_mock.cdc", [MOET_TOKEN_IDENTIFIER, 1.0, FLOW_TOKEN_IDENTIFIER, MOET_TOKEN_IDENTIFIER], PROTOCOL_ACCOUNT ) diff --git a/cadence/tests/liquidation_phase1_test.cdc b/cadence/tests/liquidation_phase1_test.cdc index da88b3d8..fcc76b9b 100644 --- a/cadence/tests/liquidation_phase1_test.cdc +++ b/cadence/tests/liquidation_phase1_test.cdc @@ -511,7 +511,7 @@ fun testManualLiquidation_repaymentVaultCollateralType() { let repayAmount = debtBalance + 0.001 let seizeAmount = (repayAmount / newPrice) * 0.99 let liqRes = _executeTransaction( - "../tests/transactions/flow-alp/pool-management/manual_liquidation_chosen_vault.cdc", + "../tests/transactions/flow-alp/helpers/manual_liquidation_chosen_vault.cdc", [pid, Type<@MOET.Vault>().identifier, FLOW_TOKEN_IDENTIFIER, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], liquidator ) @@ -567,7 +567,7 @@ fun testManualLiquidation_repaymentVaultTypeMismatch() { let repayAmount = debtBalance + 0.001 let seizeAmount = (repayAmount / newPrice) * 0.99 let liqRes = _executeTransaction( - "../tests/transactions/flow-alp/pool-management/manual_liquidation_chosen_vault.cdc", + "../tests/transactions/flow-alp/helpers/manual_liquidation_chosen_vault.cdc", [pid, Type<@MOET.Vault>().identifier, MOCK_YIELD_TOKEN_IDENTIFIER, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], liquidator ) @@ -622,7 +622,7 @@ fun testManualLiquidation_unsupportedDebtType() { let repayAmount = debtBalance + 0.001 let seizeAmount = (repayAmount / newPrice) * 0.99 let liqRes = _executeTransaction( - "../tests/transactions/flow-alp/pool-management/manual_liquidation_chosen_vault.cdc", + "../tests/transactions/flow-alp/helpers/manual_liquidation_chosen_vault.cdc", [pid, MOCK_YIELD_TOKEN_IDENTIFIER, MOCK_YIELD_TOKEN_IDENTIFIER, FLOW_TOKEN_IDENTIFIER, seizeAmount, repayAmount], liquidator ) diff --git a/cadence/tests/pool_pause_test.cdc b/cadence/tests/pool_pause_test.cdc index 40e0f6a6..d1989bda 100644 --- a/cadence/tests/pool_pause_test.cdc +++ b/cadence/tests/pool_pause_test.cdc @@ -30,6 +30,14 @@ fun setup() { depositRate: 1_000_000.0, depositCapacityCap: 1_000_000.0 ) + // Grant PROTOCOL_ACCOUNT an EGovernance cap so setPoolPauseState (cap-based) works. + let grantResult = Test.executeTransaction(Test.Transaction( + code: Test.readFile("./transactions/flow-alp/setup/grant_egovernance_cap.cdc"), + authorizers: [PROTOCOL_ACCOUNT.address, PROTOCOL_ACCOUNT.address], + signers: [PROTOCOL_ACCOUNT], + arguments: [] + )) + Test.expect(grantResult, Test.beSucceeded()) snapshot = getCurrentBlockHeight() } diff --git a/cadence/tests/position_lifecycle_unhappy_test.cdc b/cadence/tests/position_lifecycle_unhappy_test.cdc index bc56b949..8b8d84ed 100644 --- a/cadence/tests/position_lifecycle_unhappy_test.cdc +++ b/cadence/tests/position_lifecycle_unhappy_test.cdc @@ -40,12 +40,12 @@ fun testPositionLifecycleBelowMinimumDeposit() { setMinimumTokenBalancePerPosition(signer: PROTOCOL_ACCOUNT, tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, minimum: minimum) // position id to use for tests - let positionId = 0 as UInt64 + let positionId: UInt64 = 0 // user prep let user = Test.createAccount() setupMoetVault(user, beFailed: false) - mintFlow(to: user, amount: 1_000.0) + Test.expect(mintFlow(to: user, amount: 1_000.0), Test.beSucceeded()) // Grant beta access to user so they can create positions grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) diff --git a/cadence/tests/test_helpers.cdc b/cadence/tests/test_helpers.cdc index 1a1c2c0f..7adb2fb9 100644 --- a/cadence/tests/test_helpers.cdc +++ b/cadence/tests/test_helpers.cdc @@ -80,7 +80,7 @@ access(all) fun grantBetaPoolParticipantAccess(_ admin: Test.TestAccount, _ grantee: Test.TestAccount) { let signers = admin.address == grantee.address ? [admin] : [admin, grantee] let betaTxn = Test.Transaction( - code: Test.readFile("./transactions/flow-alp/pool-management/03_grant_beta.cdc"), + code: Test.readFile("./transactions/flow-alp/setup/grant_beta_cap.cdc"), authorizers: [admin.address, grantee.address], signers: signers, arguments: [] @@ -398,7 +398,7 @@ fun getIsLiquidatable(pid: UInt64): Bool { access(all) fun createAndStorePool(signer: Test.TestAccount, defaultTokenIdentifier: String, beFailed: Bool) { let createRes = _executeTransaction( - "transactions/flow-alp/pool-factory/create_and_store_pool.cdc", + "./transactions/flow-alp/setup/create_and_store_pool.cdc", [defaultTokenIdentifier], signer ) @@ -552,7 +552,7 @@ fun setPoolPauseState( pause: Bool ): Test.TransactionResult { return _executeTransaction( - "./transactions/flow-alp/pool-governance/set_pool_paused.cdc", + "./transactions/flow-alp/egovernance/set_pool_paused.cdc", [pause], signer ) @@ -697,7 +697,7 @@ fun setInsuranceSwapper( priceRatio: UFix64, ): Test.TransactionResult { let res = _executeTransaction( - "./transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc", + "./transactions/flow-alp/egovernance/set_insurance_swapper_mock.cdc", [ tokenTypeIdentifier, priceRatio, tokenTypeIdentifier, MOET_TOKEN_IDENTIFIER], signer ) @@ -710,7 +710,7 @@ fun removeInsuranceSwapper( tokenTypeIdentifier: String, ): Test.TransactionResult { let res = _executeTransaction( - "./transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc", + "./transactions/flow-alp/egovernance/remove_insurance_swapper.cdc", [ tokenTypeIdentifier], signer ) @@ -1014,8 +1014,12 @@ fun getCreditBalanceForType(details: FlowALPModels.PositionDetails, vaultType: T return 0.0 } -access(all) fun getLastPositionId(): UInt64 { +access(all) +fun getLastPositionId(): UInt64 { var openEvents = Test.eventsOfType(Type()) - let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid - return pid + if openEvents.length > 0 { + let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid + return pid + } + return 0 } diff --git a/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc b/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc deleted file mode 100644 index 22b0fe82..00000000 --- a/cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc +++ /dev/null @@ -1,18 +0,0 @@ -import "FlowALPv0" -import "FlowALPModels" - -transaction(grantee: Address) { - - prepare(admin: auth(IssueStorageCapabilityController, PublishInboxCapability) &Account) { - let poolCap: Capability = - admin.capabilities.storage.issue< - auth(FlowALPModels.EParticipant, FlowALPModels.EPosition) &FlowALPv0.Pool - >(FlowALPv0.PoolStoragePath) - - assert(poolCap.check(), message: "Failed to issue beta capability") - - admin.inbox.publish(poolCap, name: "FlowALPv0BetaCap", recipient: grantee) - } -} - - diff --git a/cadence/tests/transactions/flow-alp/egovernance/add_supported_token.cdc b/cadence/tests/transactions/flow-alp/egovernance/add_supported_token.cdc new file mode 100644 index 00000000..dcad7589 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/egovernance/add_supported_token.cdc @@ -0,0 +1,38 @@ +import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EGovernance) &Pool grants access to Pool.addSupportedToken. +/// Adds a token with a zero-rate interest curve (0% APY). +transaction( + tokenTypeIdentifier: String, + collateralFactor: UFix64, + borrowFactor: UFix64, + depositRate: UFix64, + depositCapacityCap: UFix64 +) { + let tokenType: Type + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + + prepare(signer: auth(BorrowValue) &Account) { + self.tokenType = CompositeType(tokenTypeIdentifier) + ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("No EGovernance cap found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool from EGovernance cap") + } + + execute { + self.pool.addSupportedToken( + tokenType: self.tokenType, + collateralFactor: collateralFactor, + borrowFactor: borrowFactor, + interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: 0.0), + depositRate: depositRate, + depositCapacityCap: depositCapacityCap + ) + } +} diff --git a/cadence/tests/transactions/flow-alp/egovernance/collect_insurance.cdc b/cadence/tests/transactions/flow-alp/egovernance/collect_insurance.cdc new file mode 100644 index 00000000..fbcec5d0 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/egovernance/collect_insurance.cdc @@ -0,0 +1,23 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EGovernance) &Pool grants access to Pool.collectInsurance. +transaction(tokenTypeIdentifier: String) { + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let tokenType: Type + + prepare(signer: auth(BorrowValue) &Account) { + self.tokenType = CompositeType(tokenTypeIdentifier) + ?? panic("Invalid tokenTypeIdentifier: \(tokenTypeIdentifier)") + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("No EGovernance cap found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool from EGovernance cap") + } + + execute { + self.pool.collectInsurance(tokenType: self.tokenType) + } +} diff --git a/cadence/tests/transactions/flow-alp/egovernance/collect_stability.cdc b/cadence/tests/transactions/flow-alp/egovernance/collect_stability.cdc new file mode 100644 index 00000000..0e3a226a --- /dev/null +++ b/cadence/tests/transactions/flow-alp/egovernance/collect_stability.cdc @@ -0,0 +1,23 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EGovernance) &Pool grants access to Pool.collectStability. +transaction(tokenTypeIdentifier: String) { + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let tokenType: Type + + prepare(signer: auth(BorrowValue) &Account) { + self.tokenType = CompositeType(tokenTypeIdentifier) + ?? panic("Invalid tokenTypeIdentifier: \(tokenTypeIdentifier)") + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("No EGovernance cap found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool from EGovernance cap") + } + + execute { + self.pool.collectStability(tokenType: self.tokenType) + } +} diff --git a/cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc b/cadence/tests/transactions/flow-alp/egovernance/remove_insurance_swapper.cdc similarity index 100% rename from cadence/tests/transactions/flow-alp/pool-governance/remove_insurance_swapper.cdc rename to cadence/tests/transactions/flow-alp/egovernance/remove_insurance_swapper.cdc diff --git a/cadence/tests/transactions/flow-alp/egovernance/set_deposit_limit_fraction.cdc b/cadence/tests/transactions/flow-alp/egovernance/set_deposit_limit_fraction.cdc new file mode 100644 index 00000000..a50f0d32 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/egovernance/set_deposit_limit_fraction.cdc @@ -0,0 +1,26 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EGovernance) &Pool grants access to Pool.setDepositLimitFraction. +transaction( + tokenTypeIdentifier: String, + fraction: UFix64 +) { + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let tokenType: Type + + prepare(signer: auth(BorrowValue) &Account) { + self.tokenType = CompositeType(tokenTypeIdentifier) + ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("No EGovernance cap found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool from EGovernance cap") + } + + execute { + self.pool.setDepositLimitFraction(tokenType: self.tokenType, fraction: fraction) + } +} diff --git a/cadence/tests/transactions/flow-alp/egovernance/set_dex.cdc b/cadence/tests/transactions/flow-alp/egovernance/set_dex.cdc new file mode 100644 index 00000000..6d476302 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/egovernance/set_dex.cdc @@ -0,0 +1,24 @@ +import "DeFiActions" +import "FlowALPv0" +import "FlowALPModels" +import "MockDexSwapper" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EGovernance) &Pool grants access to Pool.borrowConfig, +/// enabling the governance holder to set the DEX via the config. +/// Uses MockDexSwapper.SwapperProvider as the DEX implementation. +transaction { + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + + prepare(signer: auth(BorrowValue) &Account) { + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("No EGovernance cap found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool from EGovernance cap") + } + + execute { + self.pool.borrowConfig().setDex(MockDexSwapper.SwapperProvider()) + } +} diff --git a/cadence/tests/transactions/flow-alp/egovernance/set_insurance_rate.cdc b/cadence/tests/transactions/flow-alp/egovernance/set_insurance_rate.cdc new file mode 100644 index 00000000..deffa279 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/egovernance/set_insurance_rate.cdc @@ -0,0 +1,26 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EGovernance) &Pool grants access to Pool.setInsuranceRate. +transaction( + tokenTypeIdentifier: String, + insuranceRate: UFix64 +) { + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let tokenType: Type + + prepare(signer: auth(BorrowValue) &Account) { + self.tokenType = CompositeType(tokenTypeIdentifier) + ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("No EGovernance cap found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool from EGovernance cap") + } + + execute { + self.pool.setInsuranceRate(tokenType: self.tokenType, insuranceRate: insuranceRate) + } +} diff --git a/cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc b/cadence/tests/transactions/flow-alp/egovernance/set_insurance_swapper_mock.cdc similarity index 100% rename from cadence/tests/transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc rename to cadence/tests/transactions/flow-alp/egovernance/set_insurance_swapper_mock.cdc diff --git a/cadence/tests/transactions/flow-alp/egovernance/set_interest_curve.cdc b/cadence/tests/transactions/flow-alp/egovernance/set_interest_curve.cdc new file mode 100644 index 00000000..df35eacf --- /dev/null +++ b/cadence/tests/transactions/flow-alp/egovernance/set_interest_curve.cdc @@ -0,0 +1,31 @@ +import "FlowALPv0" +import "FlowALPModels" +import "FlowALPInterestRates" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EGovernance) &Pool grants access to Pool.setInterestCurve. +/// Sets a FixedCurve with the given yearly rate. +transaction( + tokenTypeIdentifier: String, + yearlyRate: UFix128 +) { + let tokenType: Type + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + + prepare(signer: auth(BorrowValue) &Account) { + self.tokenType = CompositeType(tokenTypeIdentifier) + ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("No EGovernance cap found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool from EGovernance cap") + } + + execute { + self.pool.setInterestCurve( + tokenType: self.tokenType, + interestCurve: FlowALPInterestRates.FixedCurve(yearlyRate: yearlyRate) + ) + } +} diff --git a/cadence/tests/transactions/flow-alp/egovernance/set_liquidation_params.cdc b/cadence/tests/transactions/flow-alp/egovernance/set_liquidation_params.cdc new file mode 100644 index 00000000..1927cc7a --- /dev/null +++ b/cadence/tests/transactions/flow-alp/egovernance/set_liquidation_params.cdc @@ -0,0 +1,23 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EGovernance) &Pool grants access to Pool.borrowConfig, +/// enabling the governance holder to set the liquidation target health factor. +/// +/// @param targetHF: The target health factor for liquidations (must be > 1.0) +transaction(targetHF: UFix128) { + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + + prepare(signer: auth(BorrowValue) &Account) { + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("No EGovernance cap found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool from EGovernance cap") + } + + execute { + self.pool.borrowConfig().setLiquidationTargetHF(targetHF) + } +} diff --git a/cadence/tests/transactions/flow-alp/egovernance/set_oracle.cdc b/cadence/tests/transactions/flow-alp/egovernance/set_oracle.cdc new file mode 100644 index 00000000..4ee39865 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/egovernance/set_oracle.cdc @@ -0,0 +1,25 @@ +import "DeFiActions" +import "FlowALPv0" +import "FlowALPModels" +import "MockOracle" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EGovernance) &Pool grants access to Pool.setPriceOracle. +/// Uses MockOracle.PriceOracle as the oracle implementation. +/// The MockOracle's unitOfAccount must match the pool's default token (MOET). +transaction { + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + + prepare(signer: auth(BorrowValue) &Account) { + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("No EGovernance cap found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool from EGovernance cap") + } + + execute { + // MockOracle.PriceOracle uses MOET as unitOfAccount, matching the pool's default token + self.pool.setPriceOracle(MockOracle.PriceOracle()) + } +} diff --git a/cadence/tests/transactions/flow-alp/egovernance/set_pause_params.cdc b/cadence/tests/transactions/flow-alp/egovernance/set_pause_params.cdc new file mode 100644 index 00000000..96055e0e --- /dev/null +++ b/cadence/tests/transactions/flow-alp/egovernance/set_pause_params.cdc @@ -0,0 +1,23 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EGovernance) &Pool grants access to Pool.borrowConfig, +/// enabling the governance holder to set the warmup period. +/// +/// @param warmupSec: Warm-up period in seconds before pause takes full effect +transaction(warmupSec: UInt64) { + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + + prepare(signer: auth(BorrowValue) &Account) { + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("No EGovernance cap found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool from EGovernance cap") + } + + execute { + self.pool.borrowConfig().setWarmupSec(warmupSec) + } +} diff --git a/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc b/cadence/tests/transactions/flow-alp/egovernance/set_pool_paused.cdc similarity index 69% rename from cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc rename to cadence/tests/transactions/flow-alp/egovernance/set_pool_paused.cdc index 7777122b..0e955e92 100644 --- a/cadence/tests/transactions/flow-alp/pool-governance/set_pool_paused.cdc +++ b/cadence/tests/transactions/flow-alp/egovernance/set_pool_paused.cdc @@ -12,8 +12,10 @@ transaction(pause: Bool) { let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool prepare(signer: auth(BorrowValue) &Account) { - self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) - ?? panic("Could not borrow Pool at \(FlowALPv0.PoolStoragePath)") + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("No EGovernance cap found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool from EGovernance cap") } execute { diff --git a/cadence/tests/transactions/flow-alp/egovernance/set_stability_fee_rate.cdc b/cadence/tests/transactions/flow-alp/egovernance/set_stability_fee_rate.cdc new file mode 100644 index 00000000..06792229 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/egovernance/set_stability_fee_rate.cdc @@ -0,0 +1,26 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EGovernance) &Pool grants access to Pool.setStabilityFeeRate. +transaction( + tokenTypeIdentifier: String, + stabilityFeeRate: UFix64 +) { + let pool: auth(FlowALPModels.EGovernance) &FlowALPv0.Pool + let tokenType: Type + + prepare(signer: auth(BorrowValue) &Account) { + self.tokenType = CompositeType(tokenTypeIdentifier) + ?? panic("Invalid tokenTypeIdentifier \(tokenTypeIdentifier)") + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("No EGovernance cap found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool from EGovernance cap") + } + + execute { + self.pool.setStabilityFeeRate(tokenType: self.tokenType, stabilityFeeRate: stabilityFeeRate) + } +} diff --git a/cadence/tests/transactions/flow-alp/eimplementation/async_update.cdc b/cadence/tests/transactions/flow-alp/eimplementation/async_update.cdc new file mode 100644 index 00000000..fa3017e3 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/eimplementation/async_update.cdc @@ -0,0 +1,22 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION β€” DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EImplementation) &Pool grants access to Pool.asyncUpdate. +/// EImplementation is never issued as an external capability β€” only the account +/// that owns the Pool in storage can access it. The queue may be empty; asyncUpdate +/// is a no-op in that case. +transaction { + let pool: auth(FlowALPModels.EImplementation) &FlowALPv0.Pool + + prepare(signer: auth(BorrowValue) &Account) { + self.pool = signer.storage.borrow( + from: FlowALPv0.PoolStoragePath + ) ?? panic("Could not borrow Pool with EImplementation entitlement") + } + + execute { + self.pool.asyncUpdate() + } +} diff --git a/cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc b/cadence/tests/transactions/flow-alp/eimplementation/async_update_position.cdc similarity index 100% rename from cadence/tests/transactions/flow-alp/pool-management/async_update_position.cdc rename to cadence/tests/transactions/flow-alp/eimplementation/async_update_position.cdc diff --git a/cadence/tests/transactions/flow-alp/eimplementation/regenerate_capacities.cdc b/cadence/tests/transactions/flow-alp/eimplementation/regenerate_capacities.cdc new file mode 100644 index 00000000..074f7cc9 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/eimplementation/regenerate_capacities.cdc @@ -0,0 +1,20 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EImplementation) &Pool grants access to Pool.regenerateAllDepositCapacities. +/// regenerateAllDepositCapacities recalculates deposit capacity for all supported token types +/// based on the configured deposit rate. Safe to call at any time. +transaction { + let pool: auth(FlowALPModels.EImplementation) &FlowALPv0.Pool + + prepare(signer: auth(BorrowValue) &Account) { + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + ?? panic("Could not borrow Pool with EImplementation entitlement") + } + + execute { + self.pool.regenerateAllDepositCapacities() + } +} diff --git a/cadence/tests/transactions/flow-alp/eparticipant/create_and_deposit_via_cap.cdc b/cadence/tests/transactions/flow-alp/eparticipant/create_and_deposit_via_cap.cdc new file mode 100644 index 00000000..ef80a72d --- /dev/null +++ b/cadence/tests/transactions/flow-alp/eparticipant/create_and_deposit_via_cap.cdc @@ -0,0 +1,54 @@ +import "FungibleToken" +import "FlowALPv0" +import "FlowALPModels" +import "MOET" +import "DummyConnectors" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that Capability (EParticipant-ONLY, fixed beta cap) grants: +/// Pool.createPosition +/// Pool.depositToPosition +/// +/// Uses the cap stored at FlowALPv0.PoolCapStoragePath. +/// +/// NOTE: All logic is in prepare because @Position resources cannot be stored as +/// transaction fields, and execute has no storage access. The prepare-only pattern +/// is correct by necessity for resource-creating transactions. +transaction { + prepare(signer: auth(BorrowValue, Storage) &Account) { + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("EParticipant-only capability not found") + + let pool = cap.borrow() ?? panic("Could not borrow Pool with EParticipant") + + let vault = signer.storage.borrow(from: MOET.VaultStoragePath) + ?? panic("No MOET vault") + + // Ensure PositionManager exists (plain borrow is sufficient for addPosition) + if signer.storage.borrow<&FlowALPv0.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { + let manager <- FlowALPv0.createPositionManager() + signer.storage.save(<-manager, to: FlowALPv0.PositionStoragePath) + } + let manager = signer.storage.borrow( + from: FlowALPv0.PositionStoragePath + ) ?? panic("No PositionManager") + + let funds <- vault.withdraw(amount: 5.0) + + // createPosition β€” requires EParticipant + let position <- pool.createPosition( + funds: <-funds, + issuanceSink: DummyConnectors.DummySink(), + repaymentSource: nil, + pushToDrawDownSink: false + ) + let pid = position.id + manager.addPosition(position: <-position) + + // depositToPosition β€” requires EParticipant + let moreFunds <- vault.withdraw(amount: 1.0) + pool.depositToPosition(pid: pid, from: <-moreFunds) + } +} diff --git a/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc b/cadence/tests/transactions/flow-alp/eparticipant/create_and_deposit_via_storage.cdc similarity index 74% rename from cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc rename to cadence/tests/transactions/flow-alp/eparticipant/create_and_deposit_via_storage.cdc index 17033aa0..50c73783 100644 --- a/cadence/tests/transactions/flow-alp/pool-management/02_positive_with_eparticipant_pass.cdc +++ b/cadence/tests/transactions/flow-alp/eparticipant/create_and_deposit_via_storage.cdc @@ -6,6 +6,15 @@ import "FlowALPModels" import "MOET" import "DummyConnectors" +/// TEST TRANSACTION β€” DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EParticipant) &Pool (issued inline as a storage capability) grants: +/// Pool.createPosition +/// Pool.depositToPosition +/// +/// NOTE: All logic is in prepare because @Position resources cannot be stored as +/// transaction fields, and execute has no storage access. The prepare-only pattern +/// is correct by necessity for resource-creating transactions. transaction { prepare(admin: auth(BorrowValue, IssueStorageCapabilityController) &Account) { let minter = admin.storage.borrow<&MOET.Minter>(from: MOET.AdminStoragePath) diff --git a/cadence/tests/transactions/flow-alp/eparticipant/create_position_via_published_cap.cdc b/cadence/tests/transactions/flow-alp/eparticipant/create_position_via_published_cap.cdc new file mode 100644 index 00000000..677938db --- /dev/null +++ b/cadence/tests/transactions/flow-alp/eparticipant/create_position_via_published_cap.cdc @@ -0,0 +1,49 @@ +import "FungibleToken" +import "FlowALPv0" +import "FlowALPModels" +import "MOET" +import "DummyConnectors" + +/// TEST TRANSACTION β€” DO NOT USE IN PRODUCTION +/// +/// Verifies that a Pool borrow with auth(EParticipant, EPosition) allows +/// Pool.createPosition and Pool.depositToPosition, creating a PositionManager +/// if one does not already exist. Used after the publishβ†’claim beta cap flow. +/// +/// NOTE: All logic is in prepare because @Position resources cannot be stored as +/// transaction fields, and execute has no storage access. The prepare-only pattern +/// is correct by necessity for resource-creating transactions. +transaction { + prepare(admin: auth(BorrowValue, Storage) &Account) { + let pool = admin.storage.borrow(from: FlowALPv0.PoolStoragePath) + ?? panic("Could not borrow Pool with EParticipant+EPosition entitlement") + + let moetVault = admin.storage.borrow(from: MOET.VaultStoragePath) + ?? panic("Could not borrow MOET vault") + + // Ensure PositionManager exists + if admin.storage.borrow<&FlowALPv0.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { + let manager <- FlowALPv0.createPositionManager() + admin.storage.save(<-manager, to: FlowALPv0.PositionStoragePath) + } + + // Pool.createPosition β€” requires EParticipant + let funds <- moetVault.withdraw(amount: 1.0) + let position <- pool.createPosition( + funds: <-funds, + issuanceSink: DummyConnectors.DummySink(), + repaymentSource: nil, + pushToDrawDownSink: false + ) + + let pid = position.id + + // Add position to manager + let manager = admin.storage.borrow(from: FlowALPv0.PositionStoragePath)! + manager.addPosition(position: <-position) + + // Pool.depositToPosition β€” requires EParticipant + let moreFunds <- moetVault.withdraw(amount: 1.0) + pool.depositToPosition(pid: pid, from: <-moreFunds) + } +} diff --git a/cadence/tests/transactions/flow-alp/eposition/deposit_and_push_any.cdc b/cadence/tests/transactions/flow-alp/eposition/deposit_and_push_any.cdc new file mode 100644 index 00000000..d0cf2308 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/eposition/deposit_and_push_any.cdc @@ -0,0 +1,33 @@ +import "FungibleToken" +import "FlowALPv0" +import "FlowALPModels" +import "MOET" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that Capability grants: +/// Pool.depositAndPush β€” on a position owned by ANOTHER user +/// +/// EPosition allows pool-level position operations on any position by ID, +/// regardless of which account owns that position. No EParticipant required. +/// +/// @param pid: Target position ID (owned by a different account) +/// @param amount: Amount of MOET to deposit +transaction(pid: UInt64, amount: UFix64) { + let pool: auth(FlowALPModels.EPosition) &FlowALPv0.Pool + let funds: @{FungibleToken.Vault} + + prepare(signer: auth(BorrowValue) &Account) { + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("EPosition capability not found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool with EPosition") + let vault = signer.storage.borrow(from: MOET.VaultStoragePath) + ?? panic("Could not borrow MOET vault with Withdraw entitlement") + self.funds <- vault.withdraw(amount: amount) + } + + execute { + self.pool.depositAndPush(pid: pid, from: <-self.funds, pushToDrawDownSink: false) + } +} diff --git a/cadence/tests/transactions/flow-alp/eposition/lock_any.cdc b/cadence/tests/transactions/flow-alp/eposition/lock_any.cdc new file mode 100644 index 00000000..685b2290 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/eposition/lock_any.cdc @@ -0,0 +1,28 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that Capability grants: +/// Pool.lockPosition β€” on ANY position +/// Pool.unlockPosition β€” on ANY position +/// +/// EPosition allows pool-level position operations on any position by ID, +/// regardless of which account owns that position. No EParticipant required. +/// +/// @param pid: Target position ID (may belong to a different account) +transaction(pid: UInt64) { + let pool: auth(FlowALPModels.EPosition) &FlowALPv0.Pool + + prepare(signer: auth(BorrowValue) &Account) { + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("EPosition capability not found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool with EPosition") + } + + execute { + self.pool.lockPosition(pid) + self.pool.unlockPosition(pid) + } +} diff --git a/cadence/tests/transactions/flow-alp/eposition/rebalance_pool.cdc b/cadence/tests/transactions/flow-alp/eposition/rebalance_pool.cdc new file mode 100644 index 00000000..8916d02f --- /dev/null +++ b/cadence/tests/transactions/flow-alp/eposition/rebalance_pool.cdc @@ -0,0 +1,28 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that Capability grants access to +/// Pool.rebalancePosition via the EPosition entitlement. +/// +/// ePositionUser holds auth(EPosition) &Pool which satisfies the EPosition | ERebalance +/// requirement of Pool.rebalancePosition. +/// +/// @param pid: Position to rebalance +/// @param force: Whether to force rebalance regardless of health bounds +transaction(pid: UInt64, force: Bool) { + let pool: auth(FlowALPModels.EPosition) &FlowALPv0.Pool + + prepare(signer: auth(BorrowValue) &Account) { + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("EPosition capability not found at PoolCapStoragePath") + self.pool = cap.borrow() ?? panic("Could not borrow Pool with EPosition") + } + + execute { + // Pool.rebalancePosition β€” requires EPosition | ERebalance; EPosition alone is sufficient + self.pool.rebalancePosition(pid: pid, force: force) + } +} diff --git a/cadence/tests/transactions/flow-alp/eposition/withdraw_and_pull_any.cdc b/cadence/tests/transactions/flow-alp/eposition/withdraw_and_pull_any.cdc new file mode 100644 index 00000000..524cb32b --- /dev/null +++ b/cadence/tests/transactions/flow-alp/eposition/withdraw_and_pull_any.cdc @@ -0,0 +1,38 @@ +import "FungibleToken" +import "FlowALPv0" +import "FlowALPModels" +import "MOET" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that Capability grants: +/// Pool.withdrawAndPull β€” on a position owned by ANOTHER user +/// +/// EPosition allows pool-level position operations on any position by ID, +/// regardless of which account owns that position. No EParticipant required. +/// +/// @param pid: Target position ID (owned by a different account) +/// @param amount: Amount to withdraw +transaction(pid: UInt64, amount: UFix64) { + let pool: auth(FlowALPModels.EPosition) &FlowALPv0.Pool + let receiver: &{FungibleToken.Receiver} + + prepare(signer: auth(BorrowValue) &Account) { + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("EPosition capability not found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool with EPosition") + self.receiver = signer.storage.borrow<&{FungibleToken.Receiver}>(from: MOET.VaultStoragePath) + ?? panic("No MOET vault receiver") + } + + execute { + let vault <- self.pool.withdrawAndPull( + pid: pid, + type: Type<@MOET.Vault>(), + amount: amount, + pullFromTopUpSource: false + ) + self.receiver.deposit(from: <-vault) + } +} diff --git a/cadence/tests/transactions/flow-alp/eposition/withdraw_any.cdc b/cadence/tests/transactions/flow-alp/eposition/withdraw_any.cdc new file mode 100644 index 00000000..50fde3b0 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/eposition/withdraw_any.cdc @@ -0,0 +1,33 @@ +import "FungibleToken" +import "FlowALPv0" +import "FlowALPModels" +import "MOET" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that Capability grants: +/// Pool.withdraw β€” on a position owned by ANOTHER user +/// +/// EPosition allows pool-level position operations on any position by ID, +/// regardless of which account owns that position. No EParticipant required. +/// +/// @param pid: Target position ID (owned by a different account) +/// @param amount: Amount to withdraw +transaction(pid: UInt64, amount: UFix64) { + let pool: auth(FlowALPModels.EPosition) &FlowALPv0.Pool + let receiver: &{FungibleToken.Receiver} + + prepare(signer: auth(BorrowValue) &Account) { + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("EPosition capability not found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool with EPosition") + self.receiver = signer.storage.borrow<&{FungibleToken.Receiver}>(from: MOET.VaultStoragePath) + ?? panic("No MOET vault receiver") + } + + execute { + let vault <- self.pool.withdraw(pid: pid, amount: amount, type: Type<@MOET.Vault>()) + self.receiver.deposit(from: <-vault) + } +} diff --git a/cadence/tests/transactions/flow-alp/epositionadmin/add_remove_position.cdc b/cadence/tests/transactions/flow-alp/epositionadmin/add_remove_position.cdc new file mode 100644 index 00000000..6cd9bbef --- /dev/null +++ b/cadence/tests/transactions/flow-alp/epositionadmin/add_remove_position.cdc @@ -0,0 +1,52 @@ +import "FungibleToken" +import "FlowALPv0" +import "FlowALPModels" +import "MOET" +import "DummyConnectors" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EPositionAdmin) &PositionManager grants access to: +/// - PositionManager.addPosition +/// - PositionManager.removePosition +/// +/// Creates a fresh position, adds it to the PositionManager, removes it, and destroys it. +/// This confirms both operations are accessible when the PositionManager is borrowed +/// with the EPositionAdmin entitlement. +/// +/// NOTE: All logic is in prepare because @Position resources cannot be stored as +/// transaction fields, and execute has no storage access. The prepare-only pattern +/// is correct by necessity for resource-creating/moving transactions. +transaction { + prepare(signer: auth(BorrowValue, Storage) &Account) { + // Create a fresh position (direct borrow since signer owns the pool) + let pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + ?? panic("Could not borrow Pool with EParticipant entitlement") + + let moetVault = signer.storage.borrow(from: MOET.VaultStoragePath) + ?? panic("Could not borrow MOET vault") + let funds <- moetVault.withdraw(amount: 1.0) + let position <- pool.createPosition( + funds: <-funds, + issuanceSink: DummyConnectors.DummySink(), + repaymentSource: nil, + pushToDrawDownSink: false + ) + let newPid = position.id + + // Get PositionManager with EPositionAdmin entitlement + let manager = signer.storage.borrow( + from: FlowALPv0.PositionStoragePath + ) ?? panic("Could not borrow PositionManager with EPositionAdmin entitlement") + + // Test addPosition (requires EPositionAdmin on PositionManager) + manager.addPosition(position: <-position) + + // Test removePosition (requires EPositionAdmin on PositionManager) + let removed <- manager.removePosition(pid: newPid) + + // Verify correctness and clean up + assert(removed.id == newPid, message: "Removed position ID does not match created position ID") + destroy removed + } +} diff --git a/cadence/tests/transactions/flow-alp/epositionadmin/borrow_authorized.cdc b/cadence/tests/transactions/flow-alp/epositionadmin/borrow_authorized.cdc new file mode 100644 index 00000000..242fecc3 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/epositionadmin/borrow_authorized.cdc @@ -0,0 +1,25 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION β€” DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EPositionAdmin) &PositionManager grants access to +/// PositionManager.borrowAuthorizedPosition, which returns an authorized +/// auth(EPositionAdmin) &Position reference. +/// +/// @param pid: The position ID to borrow an authorized reference for +transaction(pid: UInt64) { + let posRef: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position + + prepare(signer: auth(BorrowValue) &Account) { + let manager = signer.storage.borrow( + from: FlowALPv0.PositionStoragePath + ) ?? panic("Could not borrow PositionManager with EPositionAdmin entitlement") + + self.posRef = manager.borrowAuthorizedPosition(pid: pid) + } + + execute { + assert(self.posRef.id == pid, message: "Borrowed position ID does not match requested pid") + } +} diff --git a/cadence/tests/transactions/flow-alp/epositionadmin/provide_sink.cdc b/cadence/tests/transactions/flow-alp/epositionadmin/provide_sink.cdc new file mode 100644 index 00000000..c8914538 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/epositionadmin/provide_sink.cdc @@ -0,0 +1,30 @@ +import "DeFiActions" +import "FlowALPv0" +import "FlowALPModels" +import "DummyConnectors" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EPositionAdmin) &Position grants access to Position.provideSink. +/// Borrows the PositionManager with EPositionAdmin, gets an authorized Position reference, +/// and sets a DummySink as the draw-down sink (then clears it with nil). +/// +/// @param pid: The position ID whose sink should be configured +transaction(pid: UInt64) { + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position + + prepare(signer: auth(BorrowValue) &Account) { + let manager = signer.storage.borrow( + from: FlowALPv0.PositionStoragePath + ) ?? panic("Could not borrow PositionManager with EPositionAdmin entitlement") + + self.position = manager.borrowAuthorizedPosition(pid: pid) + } + + execute { + // Set a sink (DummySink accepts MOET, which is the pool's default token) + self.position.provideSink(sink: DummyConnectors.DummySink()) + // Clear it again to leave state clean + self.position.provideSink(sink: nil) + } +} diff --git a/cadence/tests/transactions/flow-alp/epositionadmin/provide_source.cdc b/cadence/tests/transactions/flow-alp/epositionadmin/provide_source.cdc new file mode 100644 index 00000000..40806509 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/epositionadmin/provide_source.cdc @@ -0,0 +1,26 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(EPositionAdmin) &Position grants access to Position.provideSource. +/// Borrows the PositionManager with EPositionAdmin, gets an authorized Position reference, +/// and clears the top-up source (nil is always a valid argument). +/// +/// @param pid: The position ID whose top-up source should be configured +transaction(pid: UInt64) { + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position + + prepare(signer: auth(BorrowValue) &Account) { + let manager = signer.storage.borrow( + from: FlowALPv0.PositionStoragePath + ) ?? panic("Could not borrow PositionManager with EPositionAdmin entitlement") + + self.position = manager.borrowAuthorizedPosition(pid: pid) + } + + execute { + // Passing nil clears any existing top-up source β€” always a valid no-op + self.position.provideSource(source: nil) + } +} diff --git a/cadence/tests/transactions/flow-alp/epositionadmin/set_max_health.cdc b/cadence/tests/transactions/flow-alp/epositionadmin/set_max_health.cdc new file mode 100644 index 00000000..7c95d0af --- /dev/null +++ b/cadence/tests/transactions/flow-alp/epositionadmin/set_max_health.cdc @@ -0,0 +1,27 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION β€” DO NOT USE IN PRODUCTION +/// +/// Verifies that storage ownership of PositionManager grants EPositionAdmin access to +/// PositionManager.borrowAuthorizedPosition and Position.setMaxHealth. +/// EPositionAdmin comes exclusively from owning the PositionManager in storage +/// and cannot be delegated as a capability. +/// +/// @param pid: Own position ID to configure +/// @param maxHealth: Maximum health factor before auto-repay is triggered +transaction(pid: UInt64, maxHealth: UFix64) { + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position + + prepare(signer: auth(BorrowValue) &Account) { + let manager = signer.storage.borrow( + from: FlowALPv0.PositionStoragePath + ) ?? panic("Could not borrow PositionManager with EPositionAdmin entitlement") + + self.position = manager.borrowAuthorizedPosition(pid: pid) + } + + execute { + self.position.setMaxHealth(maxHealth: maxHealth) + } +} diff --git a/cadence/tests/transactions/flow-alp/epositionadmin/set_min_health.cdc b/cadence/tests/transactions/flow-alp/epositionadmin/set_min_health.cdc new file mode 100644 index 00000000..4d22bf70 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/epositionadmin/set_min_health.cdc @@ -0,0 +1,27 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION β€” DO NOT USE IN PRODUCTION +/// +/// Verifies that storage ownership of PositionManager grants EPositionAdmin access to +/// PositionManager.borrowAuthorizedPosition and Position.setMinHealth. +/// EPositionAdmin comes exclusively from owning the PositionManager in storage +/// and cannot be delegated as a capability. +/// +/// @param pid: Own position ID to configure +/// @param minHealth: Minimum health factor before auto-borrow is triggered +transaction(pid: UInt64, minHealth: UFix64) { + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position + + prepare(signer: auth(BorrowValue) &Account) { + let manager = signer.storage.borrow( + from: FlowALPv0.PositionStoragePath + ) ?? panic("Could not borrow PositionManager with EPositionAdmin entitlement") + + self.position = manager.borrowAuthorizedPosition(pid: pid) + } + + execute { + self.position.setMinHealth(minHealth: minHealth) + } +} diff --git a/cadence/tests/transactions/flow-alp/epositionadmin/set_target_health.cdc b/cadence/tests/transactions/flow-alp/epositionadmin/set_target_health.cdc new file mode 100644 index 00000000..f9198d8c --- /dev/null +++ b/cadence/tests/transactions/flow-alp/epositionadmin/set_target_health.cdc @@ -0,0 +1,27 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION β€” DO NOT USE IN PRODUCTION +/// +/// Verifies that storage ownership of PositionManager grants EPositionAdmin access to +/// PositionManager.borrowAuthorizedPosition and Position.setTargetHealth. +/// EPositionAdmin comes exclusively from owning the PositionManager in storage +/// and cannot be delegated as a capability. +/// +/// @param pid: Own position ID to configure +/// @param targetHealth: Target health factor to set +transaction(pid: UInt64, targetHealth: UFix64) { + let position: auth(FlowALPModels.EPositionAdmin) &FlowALPv0.Position + + prepare(signer: auth(BorrowValue) &Account) { + let manager = signer.storage.borrow( + from: FlowALPv0.PositionStoragePath + ) ?? panic("Could not borrow PositionManager with EPositionAdmin entitlement") + + self.position = manager.borrowAuthorizedPosition(pid: pid) + } + + execute { + self.position.setTargetHealth(targetHealth: targetHealth) + } +} diff --git a/cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc b/cadence/tests/transactions/flow-alp/epositionadmin/withdraw_from_position.cdc similarity index 100% rename from cadence/tests/transactions/flow-alp/pool-management/withdraw_from_position.cdc rename to cadence/tests/transactions/flow-alp/epositionadmin/withdraw_from_position.cdc diff --git a/cadence/tests/transactions/flow-alp/erebalance/pool_rebalance_position.cdc b/cadence/tests/transactions/flow-alp/erebalance/pool_rebalance_position.cdc new file mode 100644 index 00000000..dfa26a82 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/erebalance/pool_rebalance_position.cdc @@ -0,0 +1,23 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that auth(ERebalance) &Pool grants access to Pool.rebalancePosition. +/// ERebalance is the narrower entitlement specifically for rebalancing operations, +/// distinct from the broader EPosition entitlement. +/// +/// @param pid: The position ID to rebalance +/// @param force: Whether to force rebalance regardless of health bounds +transaction(pid: UInt64, force: Bool) { + let pool: auth(FlowALPModels.ERebalance) &FlowALPv0.Pool + + prepare(signer: auth(BorrowValue) &Account) { + self.pool = signer.storage.borrow(from: FlowALPv0.PoolStoragePath) + ?? panic("Could not borrow Pool with ERebalance entitlement") + } + + execute { + self.pool.rebalancePosition(pid: pid, force: force) + } +} diff --git a/cadence/tests/transactions/flow-alp/erebalance/rebalance_pool.cdc b/cadence/tests/transactions/flow-alp/erebalance/rebalance_pool.cdc new file mode 100644 index 00000000..ffb48b71 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/erebalance/rebalance_pool.cdc @@ -0,0 +1,28 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST TRANSACTION - DO NOT USE IN PRODUCTION +/// +/// Verifies that Capability (rebalancer capability) grants: +/// Pool.rebalancePosition +/// +/// This simulates how FlowALPRebalancerv1 uses a narrow ERebalance capability +/// without being granted the broader EPosition entitlement. +/// +/// @param pid: Position to rebalance +/// @param force: Whether to force rebalance regardless of health bounds +transaction(pid: UInt64, force: Bool) { + let pool: auth(FlowALPModels.ERebalance) &FlowALPv0.Pool + + prepare(signer: auth(BorrowValue) &Account) { + let cap = signer.storage.borrow<&Capability>( + from: FlowALPv0.PoolCapStoragePath + ) ?? panic("ERebalance capability not found") + self.pool = cap.borrow() ?? panic("Could not borrow Pool with ERebalance") + } + + execute { + // Pool.rebalancePosition β€” requires EPosition | ERebalance; ERebalance alone is sufficient + self.pool.rebalancePosition(pid: pid, force: force) + } +} diff --git a/cadence/tests/transactions/flow-alp/pool-management/manual_liquidation_chosen_vault.cdc b/cadence/tests/transactions/flow-alp/helpers/manual_liquidation_chosen_vault.cdc similarity index 100% rename from cadence/tests/transactions/flow-alp/pool-management/manual_liquidation_chosen_vault.cdc rename to cadence/tests/transactions/flow-alp/helpers/manual_liquidation_chosen_vault.cdc diff --git a/cadence/tests/transactions/flow-alp/pool-management/01_negative_no_eparticipant_fail.cdc b/cadence/tests/transactions/flow-alp/pool-management/01_negative_no_eparticipant_fail.cdc deleted file mode 100644 index b0d499ea..00000000 --- a/cadence/tests/transactions/flow-alp/pool-management/01_negative_no_eparticipant_fail.cdc +++ /dev/null @@ -1,28 +0,0 @@ -import "FungibleToken" -import "DeFiActions" -import "DeFiActionsUtils" -import "FlowALPv0" -import "MOET" -import "DummyConnectors" - -/// Tries to call Pool.createPosition using a plain &Pool ref (no EParticipant). -/// This should fail at CHECKING with an access/entitlement error. -transaction { - prepare(admin: auth(BorrowValue, IssueStorageCapabilityController) &Account) { - // Issue a storage cap WITHOUT any entitlement - let cap = admin.capabilities.storage.issue<&FlowALPv0.Pool>( - FlowALPv0.PoolStoragePath - ) - let pool = cap.borrow() ?? panic("nil pool") - - - // EXPECTED: checker rejects this call (createPosition is access(EParticipant)). - let zero <- DeFiActionsUtils.getEmptyVault(Type<@MOET.Vault>()) - let _ = pool.createPosition( - funds: <- zero, - issuanceSink: DummyConnectors.DummySink(), - repaymentSource: nil, - pushToDrawDownSink: false - ) - } -} diff --git a/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc b/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc deleted file mode 100644 index 0c148e10..00000000 --- a/cadence/tests/transactions/flow-alp/pool-management/04_create_position.cdc +++ /dev/null @@ -1,38 +0,0 @@ -import "FungibleToken" -import "DeFiActions" -import "DeFiActionsUtils" -import "FlowALPv0" -import "FlowALPModels" -import "MOET" -import "DummyConnectors" - -transaction { - prepare(admin: auth(BorrowValue, Storage, Capabilities) &Account) { - let pool = admin.storage.borrow(from: FlowALPv0.PoolStoragePath) - - // Ensure PositionManager exists - if admin.storage.borrow<&FlowALPv0.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { - let manager <- FlowALPv0.createPositionManager() - admin.storage.save(<-manager, to: FlowALPv0.PositionStoragePath) - } - - // Call EParticipant-gated methods - let zero1 <- DeFiActionsUtils.getEmptyVault(Type<@MOET.Vault>()) - let position <- pool.createPosition( - funds: <- zero1, - issuanceSink: DummyConnectors.DummySink(), - repaymentSource: nil, - pushToDrawDownSink: false - ) - - let pid = position.id - - // Add position to manager - let manager = admin.storage.borrow<&FlowALPv0.PositionManager>(from: FlowALPv0.PositionStoragePath)! - manager.addPosition(position: <-position) - - // Also allowed with EParticipant: - let zero2 <- DeFiActionsUtils.getEmptyVault(Type<@MOET.Vault>()) - pool.depositToPosition(pid: pid, from: <- zero2) - } -} diff --git a/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc b/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc deleted file mode 100644 index 206d0b3f..00000000 --- a/cadence/tests/transactions/flow-alp/pool-management/05_negative_cap.cdc +++ /dev/null @@ -1,20 +0,0 @@ -import "FlowALPv0" -import "FlowALPModels" - -// Intentionally executed by a NON-ADMIN account. -// Expected: PANIC when trying to borrow a governance-authorized ref. -transaction() { - - prepare(nonAdmin: auth(Capabilities) &Account) { - // Non-admin tries to issue a capability to the *admin’s* PoolFactory path. - // This account does NOT have the PoolFactory stored at that path, so the borrow() must fail. - let badGovCap: Capability = - nonAdmin.capabilities.storage.issue( - FlowALPv0.PoolFactoryPath - ) - - // This will return nil, triggering the panic β€” which is what we WANT in this negative test. - let _ = badGovCap.borrow() - ?? panic("Negative test passed: non-admin cannot borrow governance ref to PoolFactory") - } -} diff --git a/cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc b/cadence/tests/transactions/flow-alp/setup/claim_beta_cap.cdc similarity index 100% rename from cadence/tests/transactions/flow-alp/beta/claim_and_save_beta_cap.cdc rename to cadence/tests/transactions/flow-alp/setup/claim_beta_cap.cdc diff --git a/cadence/tests/transactions/flow-alp/pool-factory/create_and_store_pool.cdc b/cadence/tests/transactions/flow-alp/setup/create_and_store_pool.cdc similarity index 100% rename from cadence/tests/transactions/flow-alp/pool-factory/create_and_store_pool.cdc rename to cadence/tests/transactions/flow-alp/setup/create_and_store_pool.cdc diff --git a/cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc b/cadence/tests/transactions/flow-alp/setup/grant_beta_cap.cdc similarity index 100% rename from cadence/tests/transactions/flow-alp/pool-management/03_grant_beta.cdc rename to cadence/tests/transactions/flow-alp/setup/grant_beta_cap.cdc diff --git a/cadence/tests/transactions/flow-alp/setup/grant_egovernance_cap.cdc b/cadence/tests/transactions/flow-alp/setup/grant_egovernance_cap.cdc new file mode 100644 index 00000000..3feb2970 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/setup/grant_egovernance_cap.cdc @@ -0,0 +1,23 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST SETUP β€” grants an EGovernance Pool capability to a governance account. +/// Simulates capability-delegated governance access to the Pool. +/// Stored at FlowALPv0.PoolCapStoragePath. +transaction { + prepare( + admin: auth(IssueStorageCapabilityController) &Account, + user: auth(Storage) &Account + ) { + let cap = admin.capabilities.storage.issue( + FlowALPv0.PoolStoragePath + ) + // Overwrite any existing cap at this path + if user.storage.type(at: FlowALPv0.PoolCapStoragePath) != nil { + user.storage.load>( + from: FlowALPv0.PoolCapStoragePath + ) + } + user.storage.save(cap, to: FlowALPv0.PoolCapStoragePath) + } +} diff --git a/cadence/tests/transactions/flow-alp/setup/grant_eparticipant_cap.cdc b/cadence/tests/transactions/flow-alp/setup/grant_eparticipant_cap.cdc new file mode 100644 index 00000000..5164db1c --- /dev/null +++ b/cadence/tests/transactions/flow-alp/setup/grant_eparticipant_cap.cdc @@ -0,0 +1,23 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST SETUP β€” grants an EParticipant-ONLY Pool capability to a user account. +/// This is the FIXED beta capability β€” no EPosition over-grant. +/// Stored at FlowALPv0.PoolCapStoragePath. +transaction { + prepare( + admin: auth(IssueStorageCapabilityController) &Account, + user: auth(Storage) &Account + ) { + let cap = admin.capabilities.storage.issue( + FlowALPv0.PoolStoragePath + ) + // Overwrite any existing cap at this path + if user.storage.type(at: FlowALPv0.PoolCapStoragePath) != nil { + user.storage.load>( + from: FlowALPv0.PoolCapStoragePath + ) + } + user.storage.save(cap, to: FlowALPv0.PoolCapStoragePath) + } +} diff --git a/cadence/tests/transactions/flow-alp/setup/grant_eposition_cap.cdc b/cadence/tests/transactions/flow-alp/setup/grant_eposition_cap.cdc new file mode 100644 index 00000000..5fee69a5 --- /dev/null +++ b/cadence/tests/transactions/flow-alp/setup/grant_eposition_cap.cdc @@ -0,0 +1,25 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST SETUP β€” grants an EPosition-ONLY Pool capability to a user account. +/// This is a narrowly-scoped capability β€” no EParticipant, so the holder cannot +/// createPosition. EPosition alone allows pool-level position operations on any +/// position by ID (withdraw, depositAndPush, lockPosition, rebalancePosition, etc.). +/// Stored at FlowALPv0.PoolCapStoragePath. +transaction { + prepare( + admin: auth(IssueStorageCapabilityController) &Account, + user: auth(Storage) &Account + ) { + let cap = admin.capabilities.storage.issue( + FlowALPv0.PoolStoragePath + ) + // Overwrite any existing cap at this path + if user.storage.type(at: FlowALPv0.PoolCapStoragePath) != nil { + user.storage.load>( + from: FlowALPv0.PoolCapStoragePath + ) + } + user.storage.save(cap, to: FlowALPv0.PoolCapStoragePath) + } +} diff --git a/cadence/tests/transactions/flow-alp/setup/grant_erebalance_cap.cdc b/cadence/tests/transactions/flow-alp/setup/grant_erebalance_cap.cdc new file mode 100644 index 00000000..cf92101e --- /dev/null +++ b/cadence/tests/transactions/flow-alp/setup/grant_erebalance_cap.cdc @@ -0,0 +1,23 @@ +import "FlowALPv0" +import "FlowALPModels" + +/// TEST SETUP β€” grants an ERebalance Pool capability to a rebalancer account. +/// Simulates how FlowALPRebalancerv1 obtains narrowly-scoped rebalancing rights. +/// Stored at FlowALPv0.PoolCapStoragePath. +transaction { + prepare( + admin: auth(IssueStorageCapabilityController) &Account, + user: auth(Storage) &Account + ) { + let cap = admin.capabilities.storage.issue( + FlowALPv0.PoolStoragePath + ) + // Overwrite any existing cap at this path + if user.storage.type(at: FlowALPv0.PoolCapStoragePath) != nil { + user.storage.load>( + from: FlowALPv0.PoolCapStoragePath + ) + } + user.storage.save(cap, to: FlowALPv0.PoolCapStoragePath) + } +} diff --git a/docs/security-permission-matrix.md b/docs/security-permission-matrix.md new file mode 100644 index 00000000..d19189fb --- /dev/null +++ b/docs/security-permission-matrix.md @@ -0,0 +1,78 @@ +# Security Permission Matrix + +Maps each entitlement to the operations it permits. For audit/security review. + +## Entitlements β†’ Actors + +| Entitlement | Actor | How granted | +|---|---|---| +| `EParticipant` | User | Published capability (`publish_beta_cap.cdc`) | +| `EPosition` | ⚠️ Protocol operator (NOT end users) | Published capability (`publish_beta_cap.cdc`) ← **over-grant** | +| `ERebalance` | Rebalancer contract | Rebalancer setup | +| `EPositionAdmin` | User (own positions only) | Storage ownership of `PositionManager` β€” cannot be delegated | +| `EGovernance` | Protocol admin | Admin account | +| `EImplementation` | Protocol internals | Never issued externally | + +## Actor Capability Matrix + +| Operation | Description | User (EParticipant) | ⚠️ User w/ EPosition (current beta) | Rebalancer (ERebalance) | User β€” own positions only (EPositionAdmin) | Governance (EGovernance) | Protocol Internal (EImplementation) | +|---|---|---|---|---|---|---|---| +| `createPosition` | Open a new position | βœ… | βœ… | | | | | +| `depositToPosition` | Deposit collateral | βœ… | βœ… | | | | | +| `withdraw` | ⚠️ Withdraw from **any** position by ID | | βœ… | | | | | +| `withdrawAndPull` | ⚠️ Withdraw from **any** position + pull top-up | | βœ… | | | | | +| `depositAndPush` | ⚠️ Push funds from **any** position | | βœ… | | | | | +| `lockPosition` | ⚠️ Freeze **any** position | | βœ… | | | | | +| `unlockPosition` | ⚠️ Unfreeze **any** position | | βœ… | | | | | +| `rebalancePosition` | Rebalance a position's health | | βœ… | βœ… | | | | +| `rebalance` (Position) | Rebalance this position | | βœ… | βœ… | | | | +| `setTargetHealth` | Set target health ratio | | | | βœ… | | | +| `setMinHealth` | Set min health before auto-borrow | | | | βœ… | | | +| `setMaxHealth` | Set max health before auto-repay | | | | βœ… | | | +| `provideSink` | Configure drawdown sink | | | | βœ… | | | +| `provideSource` | Configure top-up source | | | | βœ… | | | +| `addPosition` (Manager) | Add position to manager | | | | βœ… | | | +| `removePosition` (Manager) | Remove position from manager | | | | βœ… | | | +| `borrowAuthorizedPosition` | Borrow position with withdrawal rightsΒΉ | | | | βœ… | | | +| `pausePool` / `unpausePool` | Halt or resume all pool operations | | | | | βœ… | | +| `addSupportedToken` | Add a new collateral/borrow token | | | | | βœ… | | +| `setInterestCurve` | Configure interest rate model | | | | | βœ… | | +| `setInsuranceRate` | Set the insurance fee rate | | | | | βœ… | | +| `setStabilityFeeRate` | Set the stability fee rate | | | | | βœ… | | +| `setLiquidationParams` | Configure liquidation thresholds | | | | | βœ… | | +| `setPauseParams` | Configure pause conditions | | | | | βœ… | | +| `setDepositLimitFraction` | Cap deposits as fraction of pool | | | | | βœ… | | +| `collectInsurance` | Sweep insurance fees to treasury | | | | | βœ… | | +| `collectStability` | Sweep stability fees to treasury | | | | | βœ… | | +| `withdrawStabilityFund` | Withdraw from stability reserveΒ³ | | | | | βœ… | | +| `setDEX` / `setPriceOracle` | Set liquidation DEX or price feed | | | | | βœ… | | +| `asyncUpdate` | Process queued state updates | | | | | | βœ… | +| `asyncUpdatePosition` | Process queued update for one position | | | | | | βœ… | +| `regenerateAllDepositCapacities` | Recalculate all deposit caps | | | | | | βœ… | +| `setRecurringConfig` (Rebalancer) | Change rebalance schedule | `Configure`Β² | | | | | | +| `delete` (RebalancerPaid) | Stop and remove paid rebalancer | `Delete`Β² | | | | | | + +ΒΉ `borrowAuthorizedPosition` requires `FungibleToken.Withdraw + EPositionAdmin` β€” both required (conjunction). +Β² Contract-local entitlements in `FlowALPRebalancerv1` / `FlowALPRebalancerPaidv1`, not part of the FlowALPv0 hierarchy. Not tested in `cap_test.cdc`. Covered by `cadence/tests/paid_auto_balance_test.cdc`: `test_change_recurring_config` (positive, admin succeeds), `test_change_recurring_config_as_user` (negative, non-admin denied), `test_delete_rebalancer` (`Delete` entitlement). +Β³ `withdrawStabilityFund` requires an active stability fund (non-zero debit balance + elapsed time + non-zero fee rate). Covered by `cadence/tests/withdraw_stability_funds_test.cdc`. + +## ⚠️ Known Issue: Beta Capability Over-Grant + +`publish_beta_cap.cdc` grants `EParticipant + EPosition` to beta users (the "⚠️ User w/ EPosition" column above). + +`EPosition` is **not needed** for normal user actions (create/deposit). The ⚠️ rows above are all unlocked for beta users, meaning any beta user can withdraw funds from or freeze **any other user's position**. + +**Fix:** Remove `EPosition` from the beta capability β€” grant `EParticipant` only. + +## Test Coverage + +| Test file | What it covers | +|---|---| +| `cadence/tests/cap_test.cdc` | All `FlowALPv0.Pool` entitlements: `EParticipant`, `EParticipant+EPosition` (over-grant), `EPosition`, `ERebalance`, `EPositionAdmin`, `EGovernance`, `EImplementation` β€” one test per matrix row | +| `cadence/tests/paid_auto_balance_test.cdc` | Rebalancer-contract entitlements: `Configure` (`setRecurringConfig`), `Delete` (`delete`) | +| `cadence/tests/withdraw_stability_funds_test.cdc` | `EGovernance` β†’ `withdrawStabilityFund` (requires live stability fund state) | + +## Audit Notes + +- `rebalancePosition` / `rebalance` use `EPosition | ERebalance` β€” **either** entitlement is sufficient (union, not conjunction) +- `EImplementation` maps to `Mutate + FungibleToken.Withdraw` via the `ImplementationUpdates` entitlement mapping β€” never issued externally