-
Notifications
You must be signed in to change notification settings - Fork 2
Allow borrowing tokens from reserves #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+295
−30
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -582,12 +582,12 @@ access(all) contract FlowALPv0 { | |
| /// Sets the InternalPosition's drawDownSink. If `nil`, the Pool will not be able to push overflown value when | ||
| /// the position exceeds its maximum health. | ||
| /// | ||
| /// NOTE: If a non-nil value is provided, the Sink MUST accept MOET deposits or the operation will revert. | ||
| /// TODO(jord): precondition assumes Pool's default token is MOET, however Pool has option to specify default token in constructor. | ||
| access(EImplementation) fun setDrawDownSink(_ sink: {DeFiActions.Sink}?) { | ||
| /// The Sink MUST accept a token type that is supported by the pool (i.e. present in the pool's globalLedger). | ||
| /// Validated against the caller-supplied `supportedTypes` set (pass `globalLedger.keys` from Pool). | ||
| access(EImplementation) fun setDrawDownSink(_ sink: {DeFiActions.Sink}?, supportedTypes: {Type: Bool}) { | ||
| pre { | ||
| sink == nil || sink!.getSinkType() == Type<@MOET.Vault>(): | ||
| "Invalid Sink provided - Sink must accept MOET" | ||
| sink == nil || supportedTypes[sink!.getSinkType()] == true: | ||
| "Invalid Sink provided - Sink must accept a pool-supported token type" | ||
| } | ||
| self.drawDownSink = sink | ||
| } | ||
|
|
@@ -2690,7 +2690,9 @@ access(all) contract FlowALPv0 { | |
| // assign issuance & repayment connectors within the InternalPosition | ||
| let iPos = self._borrowPosition(pid: id) | ||
| let fundsType = funds.getType() | ||
| iPos.setDrawDownSink(issuanceSink) | ||
| var supportedTypes: {Type: Bool} = {} | ||
| for t in self.globalLedger.keys { supportedTypes[t] = true } | ||
| iPos.setDrawDownSink(issuanceSink, supportedTypes: supportedTypes) | ||
| if repaymentSource != nil { | ||
| iPos.setTopUpSource(repaymentSource) | ||
| } | ||
|
|
@@ -3776,40 +3778,52 @@ access(all) contract FlowALPv0 { | |
| let sinkCapacity = drawDownSink.minimumCapacity() | ||
| let sinkAmount = (idealWithdrawal > sinkCapacity) ? sinkCapacity : idealWithdrawal | ||
|
|
||
| // TODO(jord): we enforce in setDrawDownSink that the type is MOET -> we should panic here if that does not hold (currently silently fail) | ||
| if sinkAmount > 0.0 && sinkType == Type<@MOET.Vault>() { | ||
| let tokenState = self._borrowUpdatedTokenState(type: Type<@MOET.Vault>()) | ||
| if position.balances[Type<@MOET.Vault>()] == nil { | ||
| position.balances[Type<@MOET.Vault>()] = InternalBalance( | ||
| if sinkAmount > 0.0 { | ||
| let tokenState = self._borrowUpdatedTokenState(type: sinkType) | ||
| if position.balances[sinkType] == nil { | ||
| position.balances[sinkType] = InternalBalance( | ||
| direction: BalanceDirection.Credit, | ||
| scaledBalance: 0.0 | ||
| ) | ||
| } | ||
| // record the withdrawal and mint the tokens | ||
| // Record the withdrawal against sinkType, then issue it. | ||
| // For MOET: mint new tokens. For other tokens: withdraw from pool reserves. | ||
| let uintSinkAmount = UFix128(sinkAmount) | ||
| position.balances[Type<@MOET.Vault>()]!.recordWithdrawal( | ||
| position.balances[sinkType]!.recordWithdrawal( | ||
| amount: uintSinkAmount, | ||
| tokenState: tokenState | ||
| ) | ||
| let sinkVault <- FlowALPv0._borrowMOETMinter().mintTokens(amount: sinkAmount) | ||
|
|
||
| emit Rebalanced( | ||
| pid: pid, | ||
| poolUUID: self.uuid, | ||
| atHealth: balanceSheet.health, | ||
| amount: sinkVault.balance, | ||
| fromUnder: false | ||
| ) | ||
|
|
||
| // Push what we can into the sink, and redeposit the rest | ||
| drawDownSink.depositCapacity(from: &sinkVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) | ||
| if sinkVault.balance > 0.0 { | ||
| self._depositEffectsOnly( | ||
| if sinkType == Type<@MOET.Vault>() { | ||
| let sinkVault <- FlowALPv0._borrowMOETMinter().mintTokens(amount: sinkAmount) | ||
| emit Rebalanced( | ||
| pid: pid, | ||
| from: <-sinkVault, | ||
| poolUUID: self.uuid, | ||
| atHealth: balanceSheet.health, | ||
| amount: sinkVault.balance, | ||
| fromUnder: false | ||
| ) | ||
| drawDownSink.depositCapacity(from: &sinkVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) | ||
| if sinkVault.balance > 0.0 { | ||
| self._depositEffectsOnly(pid: pid, from: <-sinkVault) | ||
| } else { | ||
| Burner.burn(<-sinkVault) | ||
| } | ||
| } else { | ||
jordanschalm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Burner.burn(<-sinkVault) | ||
| let reserveRef = (&self.reserves[sinkType] as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}?)! | ||
| let sinkVault <- reserveRef.withdraw(amount: sinkAmount) | ||
| emit Rebalanced( | ||
| pid: pid, | ||
| poolUUID: self.uuid, | ||
| atHealth: balanceSheet.health, | ||
| amount: sinkVault.balance, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same thought here |
||
| fromUnder: false | ||
| ) | ||
| drawDownSink.depositCapacity(from: &sinkVault as auth(FungibleToken.Withdraw) &{FungibleToken.Vault}) | ||
| if sinkVault.balance > 0.0 { | ||
| self._depositEffectsOnly(pid: pid, from: <-sinkVault) | ||
| } else { | ||
| Burner.burn(<-sinkVault) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -4435,7 +4449,9 @@ access(all) contract FlowALPv0 { | |
| let pool = self.pool.borrow()! | ||
| pool.lockPosition(self.id) | ||
| let pos = pool.borrowPosition(pid: self.id) | ||
| pos.setDrawDownSink(sink) | ||
| var supportedTypes: {Type: Bool} = {} | ||
| for t in pool.getSupportedTokens() { supportedTypes[t] = true } | ||
| pos.setDrawDownSink(sink, supportedTypes: supportedTypes) | ||
| pool.unlockPosition(self.id) | ||
| } | ||
|
|
||
|
|
||
163 changes: 163 additions & 0 deletions
163
cadence/tests/rebalance_drawdown_non_default_token_test.cdc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| import Test | ||
| import BlockchainHelpers | ||
| import "FlowALPv0" | ||
|
|
||
| import "test_helpers.cdc" | ||
|
|
||
| /// Tests the drawDown rebalancing path where sinkType != defaultToken. | ||
| /// | ||
| /// Setup: | ||
| /// - Pool defaultToken = MOET | ||
| /// - MockYieldToken is the collateral token (Credit) | ||
| /// - drawDownSink accepts FLOW (not MOET) → creates FLOW Debit on position | ||
| /// | ||
| /// When the position becomes overcollateralised (MockYieldToken price rises), the | ||
| /// rebalancer borrows FLOW from pool reserves — not mint MOET — and pushes it to the | ||
| /// user's FLOW vault. The position's FLOW debit grows (more FLOW borrowed) while the | ||
| /// pool's FLOW reserves shrink. | ||
|
|
||
| access(all) let MOCK_YIELD_TOKEN_IDENTIFIER = "A.0000000000000007.MockYieldToken.Vault" | ||
|
|
||
| access(all) | ||
| fun setup() { | ||
| deployContracts() | ||
| } | ||
|
|
||
| access(all) | ||
| fun testDrawDownWithNonDefaultTokenSink() { | ||
| let YT_PRICE: UFix64 = 1.0 | ||
| let FLOW_PRICE: UFix64 = 1.0 | ||
|
|
||
| setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: FLOW_TOKEN_IDENTIFIER, price: FLOW_PRICE) | ||
| setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: FLOW_PRICE) | ||
| setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOCK_YIELD_TOKEN_IDENTIFIER, price: YT_PRICE) | ||
|
|
||
| // Pool: MOET as defaultToken; FLOW and MockYieldToken both supported as collateral | ||
| createAndStorePool(signer: PROTOCOL_ACCOUNT, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false) | ||
| addSupportedTokenZeroRateCurve( | ||
| signer: PROTOCOL_ACCOUNT, | ||
| tokenTypeIdentifier: FLOW_TOKEN_IDENTIFIER, | ||
| collateralFactor: 0.8, | ||
| borrowFactor: 1.0, | ||
| depositRate: 1_000_000.0, | ||
| depositCapacityCap: 1_000_000.0 | ||
| ) | ||
| addSupportedTokenZeroRateCurve( | ||
| signer: PROTOCOL_ACCOUNT, | ||
| tokenTypeIdentifier: MOCK_YIELD_TOKEN_IDENTIFIER, | ||
| collateralFactor: 0.8, | ||
| borrowFactor: 1.0, | ||
| depositRate: 1_000_000.0, | ||
| depositCapacityCap: 1_000_000.0 | ||
| ) | ||
|
|
||
| // Protocol deposits a large FLOW reserve position so the pool has FLOW to lend. | ||
| // pushToDrawDownSink=false: protocol does not draw down (its own sink is MOET). | ||
| let RESERVE_AMOUNT: UFix64 = 10_000.0 | ||
| transferFlowTokens(to: PROTOCOL_ACCOUNT, amount: RESERVE_AMOUNT) | ||
| setupMoetVault(PROTOCOL_ACCOUNT, beFailed: false) | ||
| createPosition(signer: PROTOCOL_ACCOUNT, amount: RESERVE_AMOUNT, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false) | ||
|
|
||
| let flowReservesAtStart = getReserveBalance(vaultIdentifier: FLOW_TOKEN_IDENTIFIER) | ||
| log("FLOW in pool reserves after protocol deposit: \(flowReservesAtStart)") | ||
|
|
||
| // User: MockYieldToken collateral, FLOW drawDownSink. | ||
| // pushToDrawDownSink=true — pool immediately draws FLOW from reserves and pushes | ||
| // to the user's FLOW vault, establishing an initial FLOW Debit on the position. | ||
| let user = Test.createAccount() | ||
| let COLLATERAL: UFix64 = 1_000.0 | ||
| transferFlowTokens(to: user, amount: 100.0) | ||
| setupMockYieldTokenVault(user, beFailed: false) | ||
| mintMockYieldToken(signer: PROTOCOL_ACCOUNT, to: user.address, amount: COLLATERAL, beFailed: false) | ||
| grantBetaPoolParticipantAccess(PROTOCOL_ACCOUNT, user) | ||
|
|
||
| let flowBeforeOpen = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! | ||
|
|
||
| let openRes = _executeTransaction( | ||
| "./transactions/flow-alp/position/create_position_yt_collateral_flow_sink.cdc", | ||
| [COLLATERAL, true], // pushToDrawDownSink=true: pool borrows FLOW immediately | ||
| user | ||
| ) | ||
| Test.expect(openRes, Test.beSucceeded()) | ||
|
|
||
| // pid=0: protocol reserve position; pid=1: user's YT-collateral position | ||
| let userPid: UInt64 = 1 | ||
|
|
||
| let flowAfterOpen = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! | ||
| let healthAfterOpen = getPositionHealth(pid: userPid, beFailed: false) | ||
| let flowReservesAfterOpen = getReserveBalance(vaultIdentifier: FLOW_TOKEN_IDENTIFIER) | ||
|
|
||
| log("User FLOW before open: \(flowBeforeOpen)") | ||
| log("User FLOW after open (initial drawDown fired): \(flowAfterOpen)") | ||
| log("Health after open (should ≈ TARGET_HEALTH): \(healthAfterOpen)") | ||
| log("FLOW reserves after open: \(flowReservesAfterOpen)") | ||
|
|
||
| // Initial drawDown fired: user received FLOW, health is at targetHealth, reserves decreased | ||
| Test.assert(flowAfterOpen > flowBeforeOpen, | ||
| message: "Expected initial drawDown to push FLOW to user, got \(flowAfterOpen) (was \(flowBeforeOpen))") | ||
| Test.assert(equalAmounts128(a: healthAfterOpen, b: INT_TARGET_HEALTH, tolerance: 0.00000001), | ||
| message: "Expected health ≈ TARGET_HEALTH (\(INT_TARGET_HEALTH)) after open, got \(healthAfterOpen)") | ||
| Test.assert(flowReservesAfterOpen < flowReservesAtStart, | ||
| message: "Expected FLOW reserves to decrease after initial drawDown") | ||
|
|
||
| let detailsBefore = getPositionDetails(pid: userPid, beFailed: false) | ||
| let flowDebitBefore = getDebitBalanceForType( | ||
| details: detailsBefore, | ||
| vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)! | ||
| ) | ||
| log("FLOW debit after open: \(flowDebitBefore)") | ||
|
|
||
| // MockYieldToken price doubles → position becomes overcollateralised (health > MAX_HEALTH) | ||
| setMockOraclePrice(signer: PROTOCOL_ACCOUNT, forTokenIdentifier: MOCK_YIELD_TOKEN_IDENTIFIER, price: YT_PRICE * 2.0) | ||
|
|
||
| let healthAfterPriceChange = getPositionHealth(pid: userPid, beFailed: false) | ||
| log("Health after YT price doubles: \(healthAfterPriceChange)") | ||
| Test.assert(healthAfterPriceChange >= INT_MAX_HEALTH, | ||
| message: "Expected health >= MAX_HEALTH (\(INT_MAX_HEALTH)) after price doubling, got \(healthAfterPriceChange)") | ||
|
|
||
| // Rebalance — drawDown path fires with sinkType=FLOW, defaultToken=MOET | ||
| rebalancePosition(signer: PROTOCOL_ACCOUNT, pid: userPid, force: true, beFailed: false) | ||
|
|
||
| let healthAfterRebalance = getPositionHealth(pid: userPid, beFailed: false) | ||
| let flowAfterRebalance = getBalance(address: user.address, vaultPublicPath: /public/flowTokenReceiver)! | ||
| let flowReservesAfterRebalance = getReserveBalance(vaultIdentifier: FLOW_TOKEN_IDENTIFIER) | ||
| let flowDebitAfter = getDebitBalanceForType( | ||
| details: getPositionDetails(pid: userPid, beFailed: false), | ||
| vaultType: CompositeType(FLOW_TOKEN_IDENTIFIER)! | ||
| ) | ||
|
|
||
| log("Health after rebalance (should ≈ TARGET_HEALTH): \(healthAfterRebalance)") | ||
| log("User FLOW after rebalance: \(flowAfterRebalance)") | ||
| log("FLOW reserves after rebalance: \(flowReservesAfterRebalance)") | ||
| log("FLOW debit after rebalance: \(flowDebitAfter)") | ||
|
|
||
| // Health pulled back to targetHealth | ||
| Test.assert(healthAfterRebalance < healthAfterPriceChange, | ||
| message: "Expected health to decrease after drawDown rebalance") | ||
| Test.assert(equalAmounts128(a: healthAfterRebalance, b: INT_TARGET_HEALTH, tolerance: 0.00000001), | ||
| message: "Expected health restored to TARGET_HEALTH (\(INT_TARGET_HEALTH)), got \(healthAfterRebalance)") | ||
|
|
||
| // User received more FLOW (pool pushed sinkType=FLOW from reserves) | ||
| Test.assert(flowAfterRebalance > flowAfterOpen, | ||
| message: "Expected user FLOW to increase after rebalance drawDown, got \(flowAfterRebalance) (was \(flowAfterOpen))") | ||
|
|
||
| // Pool FLOW reserves decreased by the drawn amount | ||
| Test.assert(flowReservesAfterRebalance < flowReservesAfterOpen, | ||
| message: "Expected FLOW reserves to decrease after drawDown, got \(flowReservesAfterRebalance) (was \(flowReservesAfterOpen))") | ||
|
|
||
| // Position's FLOW debit grew — pool borrowed more FLOW on behalf of the position | ||
| Test.assert(flowDebitAfter > flowDebitBefore, | ||
| message: "Expected FLOW debit to increase after drawDown, got \(flowDebitAfter) (was \(flowDebitBefore))") | ||
|
|
||
| // Drawn amount should match debit increase and reserve decrease (within rounding tolerance) | ||
| let drawnAmount = flowAfterRebalance - flowAfterOpen | ||
| let debitIncrease = flowDebitAfter - flowDebitBefore | ||
| let reserveDecrease = flowReservesAfterOpen - flowReservesAfterRebalance | ||
| log("FLOW drawn to user in rebalance: \(drawnAmount)") | ||
| log("FLOW debit increase: \(debitIncrease)") | ||
| log("FLOW reserve decrease: \(reserveDecrease)") | ||
| Test.assert(equalAmounts(a: drawnAmount, b: debitIncrease, tolerance: 0.01), | ||
| message: "Expected drawn amount (\(drawnAmount)) ≈ debit increase (\(debitIncrease))") | ||
| Test.assert(equalAmounts(a: drawnAmount, b: reserveDecrease, tolerance: 0.01), | ||
| message: "Expected drawn amount (\(drawnAmount)) ≈ reserve decrease (\(reserveDecrease))") | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
cadence/tests/transactions/flow-alp/position/create_position_yt_collateral_flow_sink.cdc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import "FungibleToken" | ||
| import "FlowToken" | ||
|
|
||
| import "DeFiActions" | ||
| import "FungibleTokenConnectors" | ||
| import "MockYieldToken" | ||
| import "FlowALPv0" | ||
|
|
||
| /// Opens a Position with MockYieldToken collateral and a FLOW drawDownSink. | ||
| /// | ||
| /// Demonstrates sinkType (FLOW) != defaultToken (MOET): when the position becomes | ||
| /// overcollateralised the pool borrows FLOW from reserves — not MOET — and | ||
| /// pushes it to the signer's FLOW vault. | ||
| /// | ||
| transaction(amount: UFix64, pushToDrawDownSink: Bool) { | ||
|
|
||
| let collateral: @{FungibleToken.Vault} | ||
| let sink: {DeFiActions.Sink} | ||
| let source: {DeFiActions.Source} | ||
| let positionManager: auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager | ||
| let poolCap: Capability<auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool> | ||
| let signerAccount: auth(Storage) &Account | ||
|
|
||
| prepare(signer: auth(BorrowValue, Storage, Capabilities) &Account) { | ||
| self.signerAccount = signer | ||
|
|
||
| // Withdraw MockYieldToken as collateral | ||
| let ytVault = signer.storage.borrow<auth(FungibleToken.Withdraw) &MockYieldToken.Vault>( | ||
| from: MockYieldToken.VaultStoragePath | ||
| ) ?? panic("No MockYieldToken.Vault in storage") | ||
| self.collateral <- ytVault.withdraw(amount: amount) | ||
|
|
||
| // Sink: borrowed FLOW is pushed into the signer's FLOW vault | ||
| let flowDepositCap = signer.capabilities.get<&{FungibleToken.Vault}>(/public/flowTokenReceiver) | ||
| let flowWithdrawCap = signer.capabilities.storage.issue<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>(/storage/flowTokenVault) | ||
|
|
||
| self.sink = FungibleTokenConnectors.VaultSink( | ||
| max: nil, | ||
| depositVault: flowDepositCap, | ||
| uniqueID: nil | ||
| ) | ||
| // Source: repayment of FLOW debt drawn from the signer's FLOW vault | ||
| self.source = FungibleTokenConnectors.VaultSource( | ||
| min: nil, | ||
| withdrawVault: flowWithdrawCap, | ||
| uniqueID: nil | ||
| ) | ||
|
|
||
| if signer.storage.borrow<&FlowALPv0.PositionManager>(from: FlowALPv0.PositionStoragePath) == nil { | ||
| let manager <- FlowALPv0.createPositionManager() | ||
| signer.storage.save(<-manager, to: FlowALPv0.PositionStoragePath) | ||
| let readCap = signer.capabilities.storage.issue<&FlowALPv0.PositionManager>(FlowALPv0.PositionStoragePath) | ||
| signer.capabilities.publish(readCap, at: FlowALPv0.PositionPublicPath) | ||
| } | ||
|
|
||
| self.positionManager = signer.storage.borrow<auth(FlowALPv0.EPositionAdmin) &FlowALPv0.PositionManager>( | ||
| from: FlowALPv0.PositionStoragePath | ||
| ) ?? panic("PositionManager not found") | ||
|
|
||
| self.poolCap = signer.storage.load<Capability<auth(FlowALPv0.EParticipant, FlowALPv0.EPosition) &FlowALPv0.Pool>>( | ||
| from: FlowALPv0.PoolCapStoragePath | ||
| ) ?? panic("No Pool capability at PoolCapStoragePath") | ||
| } | ||
|
|
||
| execute { | ||
| let pool = self.poolCap.borrow() ?? panic("Could not borrow Pool capability") | ||
| let position <- pool.createPosition( | ||
| funds: <-self.collateral, | ||
| issuanceSink: self.sink, | ||
| repaymentSource: self.source, | ||
| pushToDrawDownSink: pushToDrawDownSink | ||
| ) | ||
| self.positionManager.addPosition(position: <-position) | ||
| self.signerAccount.storage.save(self.poolCap, to: FlowALPv0.PoolCapStoragePath) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be the actual amount withdrawn to the sink? Given the conditional on line 3806, we might for example withdraw 100X from our position, then attempt to push that 100X to the sink, but the sink only accepts 50X, so we re-deposit the remaining 50X (net withdrawal of 50X).
I think the event should reflect the net withdrawal in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll create an issue for this to track it, and address it with the other event related tasks