Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c38635e
add security permission matrix doc
vishalchangrani Feb 19, 2026
25fd067
docs: highlight EPosition over-grant risk in permission matrix
vishalchangrani Feb 19, 2026
7a1ea97
docs: restructure matrix by actor to expose beta over-grant clearly
vishalchangrani Feb 19, 2026
2780290
docs: clarify EPositionAdmin is user acting on own positions via storage
vishalchangrani Feb 19, 2026
4265852
Merge branch 'main' into taras/177-access-control-test
mts1715 Mar 3, 2026
f9f728b
Add comprehensive entitlement/capability access-control test suite
mts1715 Mar 6, 2026
491193a
fix comments
mts1715 Mar 6, 2026
4a2ea5e
Merge remote-tracking branch 'origin/main' into taras/177-access-cont…
mts1715 Mar 6, 2026
9e0b53b
Merge remote-tracking branch 'origin/main' into taras/177-access-cont…
mts1715 Mar 10, 2026
bd0dc05
fix wrong path for capability EGovernance
mts1715 Mar 10, 2026
dee9360
move some scripts from the `helper` folder to appropriate paths
mts1715 Mar 10, 2026
1a0d372
Merge remote-tracking branch 'origin/main' into taras/177-access-cont…
mts1715 Mar 10, 2026
27dc3b5
typo fix
mts1715 Mar 10, 2026
ca275cf
fix pool_pause_test.cdc to change from direct pool storage borrow to …
mts1715 Mar 10, 2026
ff18746
fix false description statement in comments;
mts1715 Mar 16, 2026
9bfe703
fix comments "do not use in production" at transactions that can be u…
mts1715 Mar 16, 2026
63cfe76
remove doubled test scripts; add some check to test
mts1715 Mar 16, 2026
628fd86
add additional verification that the intended action did actually hap…
mts1715 Mar 16, 2026
7098d3c
added neg_* tests for Egovernance methods
mts1715 Mar 16, 2026
dcd8ddd
Merge remote-tracking branch 'origin/main' into taras/177-access-cont…
mts1715 Mar 16, 2026
218fe0f
hotfix after merge
mts1715 Mar 16, 2026
6dd9e24
Merge remote-tracking branch 'origin/main' into taras/177-access-cont…
mts1715 Mar 23, 2026
361eea0
move EGovernance neg tests to egovernance_neg/ and add without_auth v…
mts1715 Mar 25, 2026
6fd9946
Merge branch 'main' into taras/177-access-control-test
mts1715 Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions cadence/scripts/flow-alp/position_max_health.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import "FlowALPv0"
import "FlowALPPositionResources"

/// Returns the maximum health for the given position.
///
/// @param positionOwner: The account address that holds the PositionManager
/// @param pid: The position ID
access(all)
fun main(positionOwner: Address, pid: UInt64): UFix64 {
let manager = getAccount(positionOwner).capabilities
.borrow<&FlowALPPositionResources.PositionManager>(FlowALPv0.PositionPublicPath)
?? panic("Could not borrow PositionManager from \(positionOwner) at \(FlowALPv0.PositionPublicPath)")
return manager.borrowPosition(pid: pid).getMaxHealth()
}
14 changes: 14 additions & 0 deletions cadence/scripts/flow-alp/position_min_health.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import "FlowALPv0"
import "FlowALPPositionResources"

/// Returns the minimum health for the given position.
///
/// @param positionOwner: The account address that holds the PositionManager
/// @param pid: The position ID
access(all)
fun main(positionOwner: Address, pid: UInt64): UFix64 {
let manager = getAccount(positionOwner).capabilities
.borrow<&FlowALPPositionResources.PositionManager>(FlowALPv0.PositionPublicPath)
?? panic("Could not borrow PositionManager from \(positionOwner) at \(FlowALPv0.PositionPublicPath)")
return manager.borrowPosition(pid: pid).getMinHealth()
}
14 changes: 14 additions & 0 deletions cadence/scripts/flow-alp/position_target_health.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import "FlowALPv0"
import "FlowALPPositionResources"

/// Returns the target health for the given position.
///
/// @param positionOwner: The account address that holds the PositionManager
/// @param pid: The position ID
access(all)
fun main(positionOwner: Address, pid: UInt64): UFix64 {
let manager = getAccount(positionOwner).capabilities
.borrow<&FlowALPPositionResources.PositionManager>(FlowALPv0.PositionPublicPath)
?? panic("Could not borrow PositionManager from \(positionOwner) at \(FlowALPv0.PositionPublicPath)")
return manager.borrowPosition(pid: pid).getTargetHealth()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
2 changes: 1 addition & 1 deletion cadence/tests/adversarial_type_spoofing_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
2 changes: 1 addition & 1 deletion cadence/tests/async_update_position_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
1,195 changes: 1,163 additions & 32 deletions cadence/tests/cap_test.cdc

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions cadence/tests/insurance_swapper_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
)
Expand Down
6 changes: 3 additions & 3 deletions cadence/tests/liquidation_phase1_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,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
)
Expand Down Expand Up @@ -696,7 +696,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
)
Expand Down Expand Up @@ -751,7 +751,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
)
Expand Down
8 changes: 8 additions & 0 deletions cadence/tests/pool_pause_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
4 changes: 2 additions & 2 deletions cadence/tests/position_lifecycle_unhappy_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions cadence/tests/queued_deposits_integration_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ fun test_getQueuedDeposits_tracksPartialAndFullDrain() {
// up to 100 FLOW from the queue and should leave 50 still queued.
Test.moveTime(by: 3601.0)
let firstAsyncRes = _executeTransaction(
"./transactions/flow-alp/pool-management/async_update_position.cdc",
"./transactions/flow-alp/eimplementation/async_update_position.cdc",
[UInt64(0)],
PROTOCOL_ACCOUNT
)
Expand All @@ -156,7 +156,7 @@ fun test_getQueuedDeposits_tracksPartialAndFullDrain() {
// should be deposited, leaving no queued entries behind.
Test.moveTime(by: 3601.0)
let secondAsyncRes = _executeTransaction(
"./transactions/flow-alp/pool-management/async_update_position.cdc",
"./transactions/flow-alp/eimplementation/async_update_position.cdc",
[UInt64(0)],
PROTOCOL_ACCOUNT
)
Expand Down
41 changes: 33 additions & 8 deletions cadence/tests/test_helpers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
Expand Down Expand Up @@ -458,12 +458,33 @@ fun getIsLiquidatable(pid: UInt64): Bool {
return res.returnValue as! Bool
}

access(all)
fun getPositionMinHealth(positionOwner: Address, pid: UInt64): UFix64 {
let res = _executeScript("../scripts/flow-alp/position_min_health.cdc", [positionOwner, pid])
Test.expect(res, Test.beSucceeded())
return res.returnValue as! UFix64
}

access(all)
fun getPositionMaxHealth(positionOwner: Address, pid: UInt64): UFix64 {
let res = _executeScript("../scripts/flow-alp/position_max_health.cdc", [positionOwner, pid])
Test.expect(res, Test.beSucceeded())
return res.returnValue as! UFix64
}

access(all)
fun getPositionTargetHealth(positionOwner: Address, pid: UInt64): UFix64 {
let res = _executeScript("../scripts/flow-alp/position_target_health.cdc", [positionOwner, pid])
Test.expect(res, Test.beSucceeded())
return res.returnValue as! UFix64
}

/* --- Transaction Helpers --- */

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
)
Expand Down Expand Up @@ -617,7 +638,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
)
Expand Down Expand Up @@ -762,7 +783,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
)
Expand All @@ -775,7 +796,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
)
Expand Down Expand Up @@ -1097,10 +1118,14 @@ 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<FlowALPEvents.Opened>())
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
}

access(all)
Expand Down

This file was deleted.

18 changes: 0 additions & 18 deletions cadence/tests/transactions/flow-alp/beta/publish_beta_cap.cdc

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<auth(FlowALPModels.EGovernance) &FlowALPv0.Pool>>(
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
)
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have ./cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc.
I don't see an issue with having separate copies for the test transactions; in fact, it might be the correct approach, as transactions outside the test folder should adhere to stricter rules.
Just wanted to confirm this was a deliberate choice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cadence/transactions/flow-alp/pool-governance/collect_insurance.cdc: directly borrows the Pool from storage (for the pool admin)
cadence/tests/transactions/flow-alp/egovernance/collect_insurance.cdc: borrows a Capability from PoolCapStoragePath (for eGovernanceUser who only has a delegated capability, not direct storage access)

Switching cap_test.cdc to use the pool-governance version would break testEGovernance_CollectInsurance since eGovernanceUser doesn't have the pool in their storage.

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import "FlowALPv0"
import "FlowALPModels"

/// Collects accrued insurance fees for the given token type.
/// Requires an EGovernance capability at PoolCapStoragePath.
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<auth(FlowALPModels.EGovernance) &FlowALPv0.Pool>>(
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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import "FlowALPv0"
import "FlowALPModels"

/// Collects accrued stability fees for the given token type.
/// Requires an EGovernance capability at PoolCapStoragePath.
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<auth(FlowALPModels.EGovernance) &FlowALPv0.Pool>>(
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)
}
}
Loading
Loading