-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathadversarial_recursive_withdraw_source_test.cdc
More file actions
137 lines (118 loc) · 5.85 KB
/
adversarial_recursive_withdraw_source_test.cdc
File metadata and controls
137 lines (118 loc) · 5.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import Test
import BlockchainHelpers
import "MOET"
import "FlowALPv0"
import "FlowALPEvents"
import "DeFiActions"
import "DeFiActionsUtils"
import "FlowToken"
import "test_helpers.cdc"
import "FungibleToken"
access(all) let protocolAccount = Test.getAccount(0x0000000000000007)
access(all) let protocolConsumerAccount = Test.getAccount(0x0000000000000008)
access(all) let userAccount = Test.createAccount()
access(all) let flowTokenIdentifier = "A.0000000000000003.FlowToken.Vault"
access(all) let flowVaultStoragePath = /storage/flowTokenVault
access(all) let flowBorrowFactor = 1.0
access(all) let flowStartPrice = 1.0
access(all) let positionFundingAmount = 1_000.0
access(all) var snapshot: UInt64 = 0
access(all) var positionID: UInt64 = 0
access(all)
fun setup() {
deployContracts()
grantBetaPoolParticipantAccess(protocolAccount, protocolConsumerAccount)
grantBetaPoolParticipantAccess(protocolAccount, userAccount)
// Price setup
setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: flowTokenIdentifier, price: flowStartPrice)
setMockOraclePrice(signer: protocolAccount, forTokenIdentifier: MOET_TOKEN_IDENTIFIER, price: 1.0)
// Create the Pool & add FLOW as supported token
createAndStorePool(signer: protocolAccount, defaultTokenIdentifier: MOET_TOKEN_IDENTIFIER, beFailed: false)
addSupportedTokenZeroRateCurve(
signer: protocolAccount,
tokenTypeIdentifier: flowTokenIdentifier,
collateralFactor: 0.65,
borrowFactor: 1.0,
depositRate: 1_000_000.0,
depositCapacityCap: 1_000_000.0
)
// Prep user's account
setupMoetVault(userAccount, beFailed: false)
mintFlow(to: userAccount, amount: positionFundingAmount * 2.0)
snapshot = getCurrentBlockHeight()
}
access(all)
fun testRecursiveWithdrawSource() {
// Ensure we always run from the same post-setup chain state.
// This makes the test deterministic across multiple runs.
if snapshot < getCurrentBlockHeight() {
Test.reset(to: snapshot)
}
// -------------------------------------------------------------------------
// Seed pool liquidity / establish a baseline lender position
// -------------------------------------------------------------------------
// Create a separate account (user1) that funds the pool by opening a position
// with a large initial deposit. This ensures the pool has reserves available
// for subsequent borrow/withdraw paths in this test.
let user1 = Test.createAccount()
setupMoetVault(user1, beFailed: false)
mintMoet(signer: protocolAccount, to: user1.address, amount: 10000.0, beFailed: false)
mintFlow(to: user1, amount: 10000.0)
let initialDeposit1 = 10000.0
createPosition(
admin: PROTOCOL_ACCOUNT,
signer: user1,
amount: initialDeposit1,
vaultStoragePath: /storage/flowTokenVault,
pushToDrawDownSink: false
)
log("[TEST] USER1 POSITION ID: \(positionID)")
// -------------------------------------------------------------------------
// Attempt a reentrancy / recursive-withdraw scenario
// -------------------------------------------------------------------------
// Open a new position for `userAccount` using a special transaction that wires
// a *malicious* topUpSource (or wrapper behavior) designed to attempt recursion
// during `withdrawAndPull(..., pullFromTopUpSource: true)`.
//
// The goal is to prove the pool rejects the attempt (e.g. via position lock /
// reentrancy guard), rather than allowing nested withdraw/deposit effects.
let openRes = executeTransaction(
"./transactions/position-manager/create_position_reentrancy.cdc",
[positionFundingAmount, flowVaultStoragePath, false],
userAccount
)
Test.expect(openRes, Test.beSucceeded())
// Read the newly opened position id from the latest Opened event.
var evts = Test.eventsOfType(Type<FlowALPEvents.Opened>())
let openedEvt = evts[evts.length - 1] as! FlowALPEvents.Opened
positionID = openedEvt.pid
log("[TEST] Position opened with ID: \(positionID)")
// Log balances for debugging context only (not assertions).
let remainingFlow = getBalance(address: userAccount.address, vaultPublicPath: /public/flowTokenReceiver) ?? 0.0
log("[TEST] User FLOW balance after open: \(remainingFlow)")
let moetBalance = getBalance(address: userAccount.address, vaultPublicPath: MOET.VaultPublicPath) ?? 0.0
log("[TEST] User MOET balance after open: \(moetBalance)")
// -------------------------------------------------------------------------
// Trigger the vulnerable path: withdraw with pullFromTopUpSource=true
// -------------------------------------------------------------------------
// This withdrawal is intentionally oversized so it cannot be satisfied purely
// from the position’s current available balance. The pool will attempt to pull
// funds from the configured topUpSource to keep the position above minHealth.
//
// 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 = withdrawFromPosition(
signer: userAccount,
positionId: positionID,
tokenTypeIdentifier: flowTokenIdentifier,
receiverVaultStoragePath: FLOW_VAULT_STORAGE_PATH,
amount: 1500.0,
pullFromTopUpSource: true
)
Test.expect(withdrawRes, Test.beFailed())
// Log post-failure balances for debugging context.
let currentFlow = getBalance(address: userAccount.address, vaultPublicPath: /public/flowTokenReceiver) ?? 0.0
log("[TEST] User FLOW balance after failed withdraw: \(currentFlow)")
let currentMoet = getBalance(address: userAccount.address, vaultPublicPath: MOET.VaultPublicPath) ?? 0.0
log("[TEST] User MOET balance after failed withdraw: \(currentMoet)")
}