Skip to content
Open
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
205 changes: 204 additions & 1 deletion cadence/tests/fork_oracle_failure_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ import "FlowALPMath"
import "test_helpers.cdc"

access(all) let MAINNET_PROTOCOL_ACCOUNT = Test.getAccount(MAINNET_PROTOCOL_ACCOUNT_ADDRESS)
access(all) let BAND_ORACLE_ACCOUNT = Test.getAccount(MAINNET_BAND_ORACLE_ADDRESS)
access(all) let BAND_ORACLE_CONNECTORS_ACCOUNT = Test.getAccount(MAINNET_BAND_ORACLE_CONNECTORS_ADDRESS)

access(all) let MAINNET_USDF_HOLDER = Test.getAccount(MAINNET_USDF_HOLDER_ADDRESS)
access(all) let MAINNET_WETH_HOLDER = Test.getAccount(MAINNET_WETH_HOLDER_ADDRESS)

access(all) let MAINNET_MOCKED_YIELD_TOKEN_ACCOUNT = Test.getAccount(MAINNET_PROTOCOL_ACCOUNT_ADDRESS)
access(all) var snapshot: UInt64 = 0

access(all)
Expand All @@ -32,9 +36,10 @@ access(all)
fun setup() {
deployContracts()

// create pool with mock oracle
createAndStorePool(signer: MAINNET_PROTOCOL_ACCOUNT, defaultTokenIdentifier: MAINNET_MOET_TOKEN_ID, beFailed: false)

// Set initial oracle prices (baseline)
// Set initial oracle prices
setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_FLOW_TOKEN_ID, price: 1.0)
setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_USDF_TOKEN_ID, price: 1.0)
setMockOraclePrice(signer: MAINNET_PROTOCOL_ACCOUNT, forTokenIdentifier: MAINNET_WETH_TOKEN_ID, price: 2000.0)
Expand Down Expand Up @@ -517,4 +522,202 @@ fun test_flash_pump_increase_doubles_health() {
// when information sources disagree.
let healthAfterCorrection = getPositionHealth(pid: pid, beFailed: false)
Test.assert(healthAfterCorrection < 1.0, message: "Position should be underwater after pump-and-dump scenario")
}

// -----------------------------------------------------------------------------
/// test_band_oracle_stale_price tests that the protocol correctly handles stale price data returned by the Band oracle.
///
/// The test verifies 2 scenarios:
/// 1. A user attempts to open a position when the oracle price is stale, the transaction must fail.
/// 2. The oracle price is updated to a fresh value, the position creation should succeed.
// -----------------------------------------------------------------------------
access(all)
fun test_band_oracle_stale_price() {
safeReset()

updateOracleToBandOracle(signer: MAINNET_PROTOCOL_ACCOUNT)

// setup user with FLOW position
let user = Test.createAccount()
let FLOWAmount = 1000.0
transferFlowTokens(to: user, amount: FLOWAmount)
grantBetaPoolParticipantAccess(MAINNET_PROTOCOL_ACCOUNT, user)

// Case 1: try to open position with a stale oracle price, expect error
var openRes = _executeTransaction(
"../transactions/flow-alp/position/create_position.cdc",
[FLOWAmount, FLOW_VAULT_STORAGE_PATH, false],
user
)
Test.expect(openRes, Test.beFailed())
Test.assertError(openRes, errorMessage: "Price data's base timestamp 1771341870 exceeds the staleThreshold 1771345470 at current timestamp")

// Case 2: update the oracle price and retry opening the position

// update price for FLOW
let symbolPrices = {
"FLOW": 1.0
}
setBandOraclePrices(signer: BAND_ORACLE_ACCOUNT, symbolPrices: symbolPrices)

// user should now be able to open the position
openRes = _executeTransaction(
"../transactions/flow-alp/position/create_position.cdc",
[FLOWAmount, FLOW_VAULT_STORAGE_PATH, false],
user
)
Test.expect(openRes, Test.beSucceeded())
}

// -----------------------------------------------------------------------------
/// Tests how the protocol behaves when the Band oracle returns no price (nil) for a token.
///
/// The test covers three scenarios:
/// 1. The token has no oracle symbol mapping, transaction fails.
/// 2. The symbol exists but the oracle has no price for it, transaction fails.
/// 3. A valid price is provided, the position creation succeeds.
// -----------------------------------------------------------------------------
access(all)
fun test_band_oracle_nil_price() {
// add Mocked Yield token as supported token (80% CF, 90% BF)
addSupportedTokenZeroRateCurve(
signer: MAINNET_PROTOCOL_ACCOUNT,
tokenTypeIdentifier: MAINNET_MOCKED_YIELD_TOKEN_ID,
collateralFactor: 0.8,
borrowFactor: 0.9,
depositRate: 1_000_000.0,
depositCapacityCap: 1_000_000.0
)

updateOracleToBandOracle(signer: MAINNET_PROTOCOL_ACCOUNT)

// setup user account with MockYieldToken
let user = Test.createAccount()
let YIELDAmount = 1000.0
setupMockYieldTokenVault(user, beFailed: false)
mintMockYieldToken(signer: MAINNET_MOCKED_YIELD_TOKEN_ACCOUNT, to: user.address, amount: YIELDAmount, beFailed: false)

grantBetaPoolParticipantAccess(MAINNET_PROTOCOL_ACCOUNT, user)

// Case 1: try to open a position without a registered oracle symbol, expect a price error
var openRes = _executeTransaction(
"../transactions/flow-alp/position/create_position.cdc",
[YIELDAmount, MAINNET_YIELD_STORAGE_PATH, false],
user
)
Test.expect(openRes, Test.beFailed())
Test.assertError(openRes, errorMessage: "Base asset type A.6b00ff876c299c61.MockYieldToken.Vault does not have an assigned symbol")

// add a new YIELD symbol to BandOracleConnectors
addSymbolToBandOracle(
signer: BAND_ORACLE_CONNECTORS_ACCOUNT,
symbol: "YIELD",
tokenTypeIdentifier: MAINNET_MOCKED_YIELD_TOKEN_ID
)

// Case 2: try to open a position when the symbol exists but the oracle returns no price, expect price error
openRes = _executeTransaction(
"../transactions/flow-alp/position/create_position.cdc",
[YIELDAmount, MAINNET_YIELD_STORAGE_PATH, false],
user
)
Test.expect(openRes, Test.beFailed())
Test.assertError(openRes, errorMessage: "Cannot get a quote for the requested symbol pair.")

// Case 3: provide a valid oracle price, try to open position

// update price for YIELD
let symbolPrices = {
"YIELD": 1.0
}
setBandOraclePrices(signer: BAND_ORACLE_ACCOUNT, symbolPrices: symbolPrices)

// user should now be able to open the position
openRes = _executeTransaction(
"../transactions/flow-alp/position/create_position.cdc",
[YIELDAmount, MAINNET_YIELD_STORAGE_PATH, false],
user
)
Test.expect(openRes, Test.beSucceeded())
}

// -----------------------------------------------------------------------------
/// Tests that liquidation is prevented when the DEX price deviates too much
/// from the Band oracle price.
///
/// The test creates an unhealthy position and configures the DEX price so that
/// the deviation between the DEX price and the Band oracle price exceeds the
/// configured deviation threshold.
// -----------------------------------------------------------------------------
access(all)
fun test_band_oracle_dex_deviation_threshold() {
safeReset()

updateOracleToBandOracle(signer: MAINNET_PROTOCOL_ACCOUNT)

// set initial Band oracle prices for USD (MOET) and FLOW
var symbolPrices = {
"USD": 1.0,
"FLOW": 1.0
}
setBandOraclePrices(signer: BAND_ORACLE_ACCOUNT, symbolPrices: symbolPrices)

// setup moetLp account, create position
let moetLp = Test.createAccount()
let MOETAmount = 50000.0
setupMoetVault(moetLp, beFailed: false)
mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: moetLp.address, amount: MOETAmount, beFailed: false)
createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: moetLp, amount: MOETAmount, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, pushToDrawDownSink: false)

// setup user account, create position
let user = Test.createAccount()
setupMoetVault(user, beFailed: false)
let FLOWAmount = 1000.0
transferFlowTokens(to: user, amount: FLOWAmount)
createPosition(admin: MAINNET_PROTOCOL_ACCOUNT, signer: user, amount: FLOWAmount, vaultStoragePath: FLOW_VAULT_STORAGE_PATH, pushToDrawDownSink: false)

let openEvents = Test.eventsOfType(Type<FlowALPEvents.Opened>())
let pid = (openEvents[openEvents.length - 1] as! FlowALPEvents.Opened).pid

// borrow MOET against the FLOW collateral
borrowFromPosition(signer: user, positionId: pid, tokenTypeIdentifier: MAINNET_MOET_TOKEN_ID, vaultStoragePath: MAINNET_MOET_STORAGE_PATH, amount: 700.0, beFailed: false)

// make the position unhealthy by lowering the FLOW oracle price
symbolPrices = {
"FLOW": 0.7
}
setBandOraclePrices(signer: BAND_ORACLE_ACCOUNT, symbolPrices: symbolPrices)

// set a DEX price slightly different from the oracle price
//
// Oracle: $0.70
// DEX: $0.685
// deviation = |0.685-0.70|/0.685 = 2.19%
setMockDexPriceForPair(
signer: MAINNET_PROTOCOL_ACCOUNT,
inVaultIdentifier: MAINNET_FLOW_TOKEN_ID,
outVaultIdentifier: MAINNET_MOET_TOKEN_ID,
vaultSourceStoragePath: MAINNET_MOET_STORAGE_PATH,
priceRatio: 0.685
)

// tighten the allowed deviation threshold to 100 bps (1%)
setDexLiquidationConfig(signer: MAINNET_PROTOCOL_ACCOUNT, dexOracleDeviationBps: 100)

// setup liquidator account
let liquidator = Test.createAccount()
setupMoetVault(liquidator, beFailed: false)
mintMoet(signer: MAINNET_PROTOCOL_ACCOUNT, to: liquidator.address, amount: 500.0, beFailed: false)

// liquidation should now fail (2.19% > 1% threshold)
let liqRes = manualLiquidation(
signer: liquidator,
pid: pid,
debtVaultIdentifier: MAINNET_MOET_TOKEN_ID,
seizeVaultIdentifier: MAINNET_FLOW_TOKEN_ID,
seizeAmount: 140.0,
repayAmount: 100.0,
)
Test.expect(liqRes, Test.beFailed())
Test.assertError(liqRes, errorMessage: "DEX/oracle price deviation too large")
}
1 change: 0 additions & 1 deletion cadence/tests/liquidation_phase1_test.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import "MockYieldToken"
import "FlowToken"
import "FlowALPMath"

access(all) let MOCK_YIELD_TOKEN_IDENTIFIER = "A.0000000000000007.MockYieldToken.Vault"
access(all) var snapshot: UInt64 = 0

access(all)
Expand Down
46 changes: 46 additions & 0 deletions cadence/tests/test_helpers.cdc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import "MOET"

access(all) let MOET_TOKEN_IDENTIFIER = "A.0000000000000007.MOET.Vault"
access(all) let FLOW_TOKEN_IDENTIFIER = "A.0000000000000003.FlowToken.Vault"
access(all) let MOCK_YIELD_TOKEN_IDENTIFIER = "A.0000000000000007.MockYieldToken.Vault"

access(all) let FLOW_VAULT_STORAGE_PATH = /storage/flowTokenVault

access(all) let PROTOCOL_ACCOUNT = Test.getAccount(0x0000000000000007)
Expand Down Expand Up @@ -41,18 +43,23 @@ access(all) let MAINNET_WBTC_TOKEN_ID = "A.1e4aa0b87d10b141.EVMVMBridgedToken_71

access(all) let MAINNET_MOET_TOKEN_ID = "A.6b00ff876c299c61.MOET.Vault"
access(all) let MAINNET_FLOW_TOKEN_ID = "A.1654653399040a61.FlowToken.Vault"
access(all) let MAINNET_MOCKED_YIELD_TOKEN_ID = "A.6b00ff876c299c61.MockYieldToken.Vault"

// Storage paths
access(all) let MAINNET_USDF_STORAGE_PATH = /storage/EVMVMBridgedToken_2aabea2058b5ac2d339b163c6ab6f2b6d53aabedVault
access(all) let MAINNET_WETH_STORAGE_PATH = /storage/EVMVMBridgedToken_2f6f07cdcf3588944bf4c42ac74ff24bf56e7590Vault
access(all) let MAINNET_WBTC_STORAGE_PATH = /storage/EVMVMBridgedToken_717dae2baf7656be9a9b01dee31d571a9d4c9579Vault
access(all) let MAINNET_MOET_STORAGE_PATH = /storage/moetTokenVault_0x6b00ff876c299c61
access(all) let MAINNET_YIELD_STORAGE_PATH = /storage/mockYieldTokenVault_0x6b00ff876c299c61

access(all) let MAINNET_PROTOCOL_ACCOUNT_ADDRESS: Address = 0x6b00ff876c299c61
access(all) let MAINNET_USDF_HOLDER_ADDRESS: Address = 0xf18b50870aed46ad
access(all) let MAINNET_WETH_HOLDER_ADDRESS: Address = 0xf62e3381a164f993
access(all) let MAINNET_WBTC_HOLDER_ADDRESS: Address = 0x47f544294e3b7656

access(all) let MAINNET_BAND_ORACLE_ADDRESS: Address = 0x6801a6222ebf784a
access(all) let MAINNET_BAND_ORACLE_CONNECTORS_ADDRESS: Address = 0xe36ef556b8b5d955

/* --- Test execution helpers --- */

access(all)
Expand Down Expand Up @@ -419,6 +426,45 @@ fun setMockOraclePrice(signer: Test.TestAccount, forTokenIdentifier: String, pri
Test.expect(setRes, Test.beSucceeded())
}

access(all)
fun updateOracleToBandOracle(signer: Test.TestAccount) {
let setRes = _executeTransaction(
"../transactions/flow-alp/pool-governance/update_oracle.cdc",
[],
signer
)
Test.expect(setRes, Test.beSucceeded())
}

/// Sets multiple BandOracle prices at once
access(all)
fun setBandOraclePrices(signer: Test.TestAccount, symbolPrices: {String: UFix64}) {
let symbolsRates: {String: UInt64} = {}
for symbol in symbolPrices.keys {
// BandOracle uses 1e9 multiplier for prices
// e.g., $1.00 = 1_000_000_000, $0.50 = 500_000_000
let price = symbolPrices[symbol]!
symbolsRates[symbol] = UInt64(price * 1_000_000_000.0)
}

let setRes = _executeTransaction(
"./transactions/band-oracle/update_data.cdc",
[symbolsRates],
signer
)
Test.expect(setRes, Test.beSucceeded())
}

access(all)
fun addSymbolToBandOracle(signer: Test.TestAccount, symbol: String, tokenTypeIdentifier: String) {
let setRes = _executeTransaction(
"../../FlowActions/cadence/transactions/band-oracle-connector/add_symbol.cdc",
[symbol, tokenTypeIdentifier],
signer
)
Test.expect(setRes, Test.beSucceeded())
}

/// Sets a swapper for the given pair with the given price ratio.
/// This overwrites any previously stored swapper for this pair, if any exists.
/// This is intended to be used in tests both to set an initial DEX price for a supported token,
Expand Down
24 changes: 24 additions & 0 deletions cadence/tests/transactions/band-oracle/update_data.cdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import "BandOracle"

/// TEST TRANSACTION - NOT FOR USE IN PRODUCTION
/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
///
/// Updates the BandOracle contract with the provided prices
///
/// @param symbolsRates: Mapping of symbols to prices where prices are USD-rate multiplied by 1e9
///
transaction(symbolsRates: {String: UInt64}) {
let updater: &BandOracle.BandOracleAdmin
prepare(signer: auth(BorrowValue) &Account) {
self.updater = signer.storage.borrow<&BandOracle.BandOracleAdmin>(from: BandOracle.OracleAdminStoragePath)
?? panic("Could not find DataUpdater at \(BandOracle.OracleAdminStoragePath)")
}
execute {
self.updater.updateData(
symbolsRates: symbolsRates,
resolveTime: UInt64(getCurrentBlock().timestamp),
requestID: revertibleRandom<UInt64>(),
relayerID: revertibleRandom<UInt64>()
)
}
}
10 changes: 10 additions & 0 deletions flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,16 @@
}
},
"dependencies": {
"BandOracle": {
"source": "mainnet://6801a6222ebf784a.BandOracle",
"hash": "ababa195ef50b63d71520022aa2468656a9703b924c0f5228cfaa51a71db094d",
"aliases": {
"mainnet": "6801a6222ebf784a",
"mainnet-fork": "6801a6222ebf784a",
"testing": "0000000000000007",
"testnet": "9fb6606c300b5051"
}
},
"Burner": {
"source": "mainnet://f233dcee88fe0abe.Burner",
"hash": "71af18e227984cd434a3ad00bb2f3618b76482842bae920ee55662c37c8bf331",
Expand Down
Loading