Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions cadence/contracts/FlowALPModels.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -1037,13 +1037,13 @@ access(all) contract FlowALPModels {
/// (used when deposits are made)
access(EImplementation) fun consumeDepositCapacity(_ amount: UFix64, pid: UInt64)

/// Returns the per-deposit limit based on depositCapacity * depositLimitFraction
/// Rationale: cap per-deposit size to a fraction of the time-based
/// depositCapacity so a single large deposit cannot monopolize capacity.
/// Returns the per-deposit limit based on user deposit limit and available deposit capacity.
/// Rationale: cap per-deposit size to a fraction of the total depositCapacityCap
/// so a single large deposit cannot monopolize capacity.
/// Excess is queued and drained in chunks (see asyncUpdatePosition),
/// enabling fair throughput across many deposits in a block. The 5%
/// fraction is conservative and can be tuned by protocol parameters.
access(EImplementation) view fun depositLimit(): UFix64
access(EImplementation) view fun depositLimit(pid: UInt64): UFix64

/// Updates interest indices and regenerates deposit capacity for elapsed time
access(EImplementation) fun updateForTimeChange()
Expand Down Expand Up @@ -1376,9 +1376,15 @@ access(all) contract FlowALPModels {
)
}

/// Returns the per-deposit limit based on depositCapacity * depositLimitFraction.
access(EImplementation) view fun depositLimit(): UFix64 {
return self.depositCapacity * self.depositLimitFraction
/// Returns the maximum amount that can be deposited to the given position without being queued.
access(EImplementation) view fun depositLimit(pid: UInt64): UFix64 {
let userCap = self.getUserDepositLimitCap()
let userUsed = self.getDepositUsageForPosition(pid)
var available = userCap - userUsed
if self.depositCapacity < available {
available = self.depositCapacity
}
return available
}

/// Updates interest indices and regenerates deposit capacity for elapsed time.
Expand Down
29 changes: 9 additions & 20 deletions cadence/contracts/FlowALPv0.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -1113,31 +1113,20 @@ access(all) contract FlowALPv0 {

// Time-based state is handled by the tokenState() helper function

// Deposit rate limiting: prevent a single large deposit from monopolizing capacity.
// Deposit rate limiting: prevent a single user or single large deposit from monopolizing capacity.
// Excess is queued to be processed asynchronously (see asyncUpdatePosition).
let depositAmount = from.balance
let depositLimit = tokenState.depositLimit()
let depositLimit = tokenState.depositLimit(pid: pid)

// depositAmount is bounded by the smaller of:
// User deposit limit, per-deposit limit, and global deposit capacity
// If the deposit would exceed a limit, queue or reject the excess
if depositAmount > depositLimit {
// The deposit is too big, so we need to queue the excess
let queuedDeposit <- from.withdraw(amount: depositAmount - depositLimit)

let excessAmount = depositAmount - depositLimit
let queuedDeposit <- from.withdraw(amount: excessAmount)
position.depositToQueue(type, vault: <-queuedDeposit)
}

// Per-user deposit limit: check if user has exceeded their per-user limit
let userDepositLimitCap = tokenState.getUserDepositLimitCap()
let currentUsage = tokenState.getDepositUsageForPosition(pid)
let remainingUserLimit = userDepositLimitCap - currentUsage

// If the deposit would exceed the user's limit, queue or reject the excess
if from.balance > remainingUserLimit {
let excessAmount = from.balance - remainingUserLimit
let queuedForUserLimit <- from.withdraw(amount: excessAmount)

position.depositToQueue(type, vault: <-queuedForUserLimit)
}

// If this position doesn't currently have an entry for this token, create one.
if position.getBalance(type) == nil {
position.setBalance(type, FlowALPModels.InternalBalance(
Expand Down Expand Up @@ -1176,7 +1165,7 @@ access(all) contract FlowALPv0 {
pid: pid,
poolUUID: self.uuid,
vaultType: type,
amount: amount,
amount: acceptedAmount,
depositedUUID: depositedUUID
)

Expand Down Expand Up @@ -1861,7 +1850,7 @@ access(all) contract FlowALPv0 {
let queuedVault <- position.removeQueuedDeposit(depositType)!
let queuedAmount = queuedVault.balance
let depositTokenState = self._borrowUpdatedTokenState(type: depositType)
let maxDeposit = depositTokenState.depositLimit()
let maxDeposit = depositTokenState.depositLimit(pid: pid)

if maxDeposit >= queuedAmount {
// We can deposit all of the queued deposit, so just do it and remove it from the queue
Expand Down
47 changes: 34 additions & 13 deletions cadence/tests/deposit_capacity_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ fun test_per_user_deposit_limits() {
// Calculate expected per-user limit
let expectedUserLimit = initialCap * depositLimitFraction // 10000 * 0.05 = 500

var capacityInfo = getDepositCapacityInfo(vaultIdentifier: MOET_TOKEN_IDENTIFIER)
Test.assertEqual(initialCap, capacityInfo["depositCapacity"]!)

// Setup user 1
let user1 = Test.createAccount()
setupMoetVault(user1, beFailed: false)
Expand All @@ -124,15 +127,23 @@ fun test_per_user_deposit_limits() {
// User 1 deposits more (should be accepted up to limit)
let user1Deposit1 = 300.0 // After this: usage = 400 (out of 500 limit)
depositToPosition(signer: user1, positionID: 0, amount: user1Deposit1, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false)


capacityInfo = getDepositCapacityInfo(vaultIdentifier: MOET_TOKEN_IDENTIFIER)
Test.assertEqual(initialCap - 400.0, capacityInfo["depositCapacity"]!)

// User 1 deposits more (should be partially accepted, partially queued)
let user1Deposit2 = 200.0 // Only 100 more can be accepted to reach limit of 500, 100 will be queued
depositToPosition(signer: user1, positionID: 0, amount: user1Deposit2, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false)
// After this: usage = 500 (at limit), 100 queued
capacityInfo = getDepositCapacityInfo(vaultIdentifier: MOET_TOKEN_IDENTIFIER)
Test.assertEqual(initialCap - 500.0, capacityInfo["depositCapacity"]!)

// User 1 tries to deposit more (should be queued due to per-user limit)
let user1Deposit3 = 100.0 // This should be queued (user already at limit)
depositToPosition(signer: user1, positionID: 0, amount: user1Deposit3, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false) // Transaction succeeds but deposit is queued
depositToPosition(signer: user1, positionID: 0, amount: user1Deposit3, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false)
// Transaction succeeds but deposit is queued
capacityInfo = getDepositCapacityInfo(vaultIdentifier: MOET_TOKEN_IDENTIFIER)
Test.assertEqual(initialCap - 500.0, capacityInfo["depositCapacity"]!)

// Setup user 2 - they should have their own independent limit
let user2 = Test.createAccount()
Expand All @@ -141,25 +152,35 @@ fun test_per_user_deposit_limits() {

let initialDeposit2 = 100.0
createPosition(admin: PROTOCOL_ACCOUNT, signer: user2, amount: initialDeposit2, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false)
// After position creation: usage = 100 (out of 500 limit)
// After position creation: user usage = 100 (out of 500 limit); total usage is 600
capacityInfo = getDepositCapacityInfo(vaultIdentifier: MOET_TOKEN_IDENTIFIER)
Test.assertEqual(initialCap - 600.0, capacityInfo["depositCapacity"]!)

// User 2 should be able to deposit up to their own limit (500 total, so 400 more)
let user2Deposit = 400.0
depositToPosition(signer: user2, positionID: 1, amount: user2Deposit, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false)
// After this: usage = 500 (at limit)

// Verify that both users have independent limits by checking capacity
// Get capacity after all deposits
var capacityInfo = getDepositCapacityInfo(vaultIdentifier: MOET_TOKEN_IDENTIFIER)
let finalCapacity = capacityInfo["depositCapacity"]!
// After this: user usage = 500 (at limit); total usage is 1000
capacityInfo = getDepositCapacityInfo(vaultIdentifier: MOET_TOKEN_IDENTIFIER)
Test.assertEqual(initialCap - 1000.0, capacityInfo["depositCapacity"]!)

// Setup user 3 - they should be able to deposit up to the user limit in a single deposit
let user3 = Test.createAccount()
setupMoetVault(user3, beFailed: false)
mintMoet(signer: PROTOCOL_ACCOUNT, to: user3.address, amount: 10000.0, beFailed: false)

let initialDeposit3 = 500.0
createPosition(admin: PROTOCOL_ACCOUNT, signer: user3, amount: initialDeposit3, vaultStoragePath: MOET.VaultStoragePath, pushToDrawDownSink: false)
// After position creation: user usage = 500 (out of 500 limit); total usage is 1500
capacityInfo = getDepositCapacityInfo(vaultIdentifier: MOET_TOKEN_IDENTIFIER)
Test.assertEqual(initialCap - 1500.0, capacityInfo["depositCapacity"]!)

// Total accepted deposits:
// user1 = 500 (100 initial + 300 + 100 from deposit2, 100 from deposit2 queued, 100 from deposit3 queued)
// user2 = 500 (100 initial + 400)
// total = 1000
// We need to check that capacity decreased by at least 1000 from some initial value
Test.assert(finalCapacity <= initialCap - 1000.0,
message: "Final capacity \(finalCapacity) should be <= initial cap \(initialCap) - 1000")
// user3 = 500 (500 initial)
// total = 1500
// since queued deposits do not consume any deposit capacity, and none of the deposits should have been
// affected by the per-deposit limit, the total capacity should have decreased by exactly 1500.0
}

// -----------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion cadence/tests/fork_liquidation_edge_cases.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ fun testStabilityAndInsuranceFees_notCollectedForLiquidatedFunds() {
// hardcodes MOET_TOKEN_ID = "A.0000000000000007.MOET.Vault" (local test address),
// whereas in fork mode MOET lives at 0x6b00ff876c299c61 (MAINNET_MOET_TOKEN_ID).
let swapRes = _executeTransaction(
"./transactions/flow-alp/pool-governance/set_insurance_swapper_mock.cdc",
"./transactions/flow-alp/egovernance/set_insurance_swapper_mock.cdc",
[MAINNET_USDF_TOKEN_ID, 1.0, MAINNET_USDF_TOKEN_ID, MAINNET_MOET_TOKEN_ID],
MAINNET_PROTOCOL_ACCOUNT
)
Expand Down
10 changes: 5 additions & 5 deletions docs/deposit_capacity_mechanism.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ After 1 hour regeneration (cap increases to 11000.0):
When a user attempts to deposit:

1. **Per-Deposit Limit Check** (prevents single large deposits):
- Calculate: `depositLimit = depositCapacity * depositLimitFraction`
- Calculate: `depositLimit = min(depositCapacity, depositCapacityCap * depositLimitFraction)`
- If deposit amount > `depositLimit`, queue the excess
- This ensures no single deposit can monopolize available capacity

Expand Down Expand Up @@ -161,10 +161,10 @@ When a user attempts to deposit:

### Per-Deposit Limit vs Per-User Limit

- **Per-Deposit Limit**: `depositCapacity * depositLimitFraction`
- Based on current available capacity
- Prevents single large deposits
- Changes as capacity is consumed
- **Per-Deposit Limit**: `min(depositCapacity, depositCapacityCap * depositLimitFraction)`
- Based on maximum capacity cap
- Prevents single large deposits and enforces the total capacity
- Limited by available capacity

- **Per-User Limit**: `depositCapacityCap * depositLimitFraction`
- Based on maximum capacity cap
Expand Down
Loading