Closed sherlock-admin2 closed 7 months ago
Request PoC to facilitate discussion between sponsor and watson
Sponsor comments:
From practice, it's not true. Pyth Network price feeds update once per second
PoC requested from @securitygrid
Requests remaining: 11
1 comment(s) were left on this issue during the judging contest.
takarez commented:
valid: this semm valid to me; medium(10)
copy the following POC to test/unit/Common/CancelOrder.t.sol:
function test_170() public {
setWethPrice(2000e8);
skip(120);
// First deposit mint doesn't use offchain oracle price
announceAndExecuteDeposit({
traderAccount: alice,
keeperAccount: keeper,
depositAmount: 100e18,
oraclePrice: 2000e8,
keeperFeeAmount: 0
});
announceOpenLeverage({traderAccount: alice, margin: 100e18, additionalSize: 100e18, keeperFeeAmount: 0});
//keeper got offchain price before order.executableAtTime
skip(vaultProxy.minExecutabilityAge() - 1);
bytes[] memory priceUpdateData = getPriceUpdateData(2000e8);
//In order to compete for tradeFee, the keeper must execute orders as quickly as possible.
skip(vaultProxy.minExecutabilityAge());
vm.prank(keeper);
delayedOrderProxy.executeOrder{value: 1}(alice, priceUpdateData);
}
function test_170_for_sponor() public {
//From sponor's comment: From practice, it's not true. Pyth Network price feeds update once per second
setWethPrice(2000e8);
skip(120);
// First deposit mint doesn't use offchain oracle price
announceAndExecuteDeposit({
traderAccount: alice,
keeperAccount: keeper,
depositAmount: 100e18,
oraclePrice: 2000e8,
keeperFeeAmount: 0
});
announceOpenLeverage({traderAccount: alice, margin: 100e18, additionalSize: 100e18, keeperFeeAmount: 0});
//keeper got offchain price before order.executableAtTime
skip(vaultProxy.minExecutabilityAge() - 1);
bytes[] memory priceUpdateDataOld = getPriceUpdateData(2000e8);
//In order to compete for tradeFee, the keeper must execute orders as quickly as possible.
skip(vaultProxy.minExecutabilityAge());
//In same block, other keeper or other project updates price. Therefore, such a situation is ok.
bytes[] memory priceUpdateDataNew = getPriceUpdateData(2000e8);
mockPyth.updatePriceFeeds{value: 1}(priceUpdateDataNew);
//keeper executes order.
vm.prank(keeper);
delayedOrderProxy.executeOrder{value: 1}(alice, priceUpdateDataOld);
}
/**output
Running 2 tests for test/unit/Common/CancelOrder.t.sol:CancelDepositTest
[FAIL. Reason: PriceStale(1)] test_170() (gas: 1329703)
[PASS] test_170_for_sponor() (gas: 1347073)
Test result: FAILED. 1 passed; 1 failed; 0 skipped; finished in 29.11ms
**/
hey, as far as I understood this whole report is based on the scenario that keeper monitors OrderAnnounced
events and then immediately after queries pyth API to get the price update message and then waits minExecutabilityAge
before execution.
this is the wrong flow, as our keeper monitors events and does not query anything before minExecutabilityAge
. pyth API is fetched after minExecutabilityAge
and then call to execute order is made.
@D-Ig
A pending order can be executed at [t0,t1]. The average block time of base is 2 seconds. A well-developed keeper can make tx to be mined at t0. The purpose is to get keeperFee(First come, first served). Therefore, the price must be obtained before t0.
this is the wrong flow, as our keeper monitors events and does not query anything before minExecutabilityAge. pyth API is fetched after minExecutabilityAge and then call to execute order is made.
If the keeper can only come from the protocol, then I will not submit this report. The protocol hopes that that the more keepers the better. Please reconsider this issue, thank you.
Hi @rashtrakoff @D-Ig any further comments on the above highlighted comment?
Hi @rashtrakoff @D-Ig any further comments on the above highlighted comment?
I don't know what else to add. for me it's a chicken and egg problem
Let's take a look at the statement which might create an issue:
`timestamp + maxAge < block.timestamp`
Since maxAge = block.timestamp - order.executableAtTime
This can also be re-written as:
`timestamp < order.executableAtTime`
Basically what I mean is that the price fetched from pyth API should be newer than executable at time. I believe this is what we intended anyway so I wouldn't consider this as an issue.
A pending order can be executed at [t0,t1].
The problem described in this report is that the order can never be executed at t0. Because it takes time for a tx to be created, sent to the node and finally mined. The price timestamp is obtained when tx is created. Therefore it is smaller than t0.
Escalate
Invalid
Keepers from protocol team will work.
After minExecutabilityAge seconds, the keeper executes the order via DelayedOrder.executeOrder. The priceUpdateData argument is obtained in step 2. Eventually tx will revert.
The fact that other keepers won't work is their desing problem.
Escalate
Invalid
Keepers from protocol team will work.
After minExecutabilityAge seconds, the keeper executes the order via DelayedOrder.executeOrder. The priceUpdateData argument is obtained in step 2. Eventually tx will revert.
The fact that other keepers won't work is their desing problem.
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.
Disagree with this escalation. The purpose of the protocol's own keeper is only to ensure that the system can operate. The purpose of the third-party keepers is only to make a profit, that is, to compete for keeperFee (first come, first served). I have fully described my views in the report/coded POC/previous comments. In the current implementation, an order cannot be executed at t0, even though the keeper takes a fresh price. No more comments, leave it to Sherlock to judge, thank you
I think this reports highlights a potential mismatch in incentives for off chain components. Seems like the protocol can function in a healthy way without implementing the proposed change. That makes it a design choice.
Planning to accept escalation and invalidate.
Fair enough @Evert0x can be low severity given sponsor comments here as well
Result: Low Has duplicates
nobody2018
medium
In executeOrder, OracleModule.getPrice(maxAge) may revert because maxAge is too small
Summary
[executeOrder](https://github.com/sherlock-audit/2023-12-flatmoney/blob/main/flatcoin-v1/src/DelayedOrder.sol#L378-L381) will call different functions to process the order according to the type of order, and these functions will call
OracleModule.getPrice(maxAge)
to get the price.maxAge
is equal to the currentblock.timestamp - order.executableAtTime
. IfmaxAge
is too small (for example, 0-3), thenOracleModule.getPrice(maxAge)
may revert [here](https://github.com/sherlock-audit/2023-12-flatmoney/blob/main/flatcoin-v1/src/OracleModule.sol#L132) even though the price is fresh.Vulnerability Detail
Below we use the
LeverageAdjust
type Order to describe this issue.DelayedOrder.announceLeverageAdjust
. Because if the price of the collateral continues to dump, position A will be liquidated. A pendingLeverageAdjust
order is created. Assumeblock.timestamp = 1707000000
, soorder.executableAtTime = block.timestamp + minExecutabilityAge = 1707000005
. The expiration time of this order isorder.executableAtTime + maxExecutabilityAge = 1707000065
.keeperFee
paid to the keeper, so the keepers will compete with each other as long as there is an order that can be executed. One keeper monitored theFlatcoinEvents.OrderAnnounced
event, it requested the API to obtainpriceUpdateData
. Assume that the fresh price'spublishTime
is 1707000003.DelayedOrder.executeOrder
. ThepriceUpdateData
argument is obtained in step 2. Eventually tx will revert.Let’s analyze the reasons for revert. The call stack of [DelayedOrder.executeOrder](https://github.com/sherlock-audit/2023-12-flatmoney/blob/main/flatcoin-v1/src/DelayedOrder.sol#L378-L381) is as follows:
L152,
maxAge = block.timestamp - _order.executableAtTime = 1707000005 - 1707000005 = 0
L159, the
maxAge
argument passed intoOracleModule.getPrice
is 0.L107, onchainTime =
updatedAt
returned byoracle.latestRoundData()
from chainlink.updatedAt
depends on the symbol's heartBeat. The heartbeats of almost chainlink price feeds are based on hours (such as 24 hours, 1 hour, etc.). Therefore,onchainTime
is always lagging time, and the probability of being equal toblock.timestamp
is low.L108,
offchainTime = publishTime of priceUpdateData = 1707000003
L117-124,
timestamp = max(onchainTime, offchainTime)
, in most casesoffchainTime
is larger.L131, in this case
maxAge=0
,So if statement is met, tx revert.
Impact
FlatcoinErrors.PriceStale
.Code Snippet
https://github.com/sherlock-audit/2023-12-flatmoney/blob/main/flatcoin-v1/src/LeverageModule.sol#L94-L96
https://github.com/sherlock-audit/2023-12-flatmoney/blob/main/flatcoin-v1/src/LeverageModule.sol#L159-L161
https://github.com/sherlock-audit/2023-12-flatmoney/blob/main/flatcoin-v1/src/LeverageModule.sol#L270-L272
Tool used
Manual Review
Recommendation
The cases in the report need to be considered.