-
Notifications
You must be signed in to change notification settings - Fork 1
WATCHPUG - OracleVersion latestVersion
of Oracle.status()
may go backwards when updating to a new oracle provider and result in wrong settlement in _processPositionLocal()
.
#145
Comments
2 comment(s) were left on this issue during the judging contest. 141345 commented:
panprog commented:
|
OracleVersion latestVersion
of Oracle.status()
may go backwards when updating to a new oracle provider and result in wrong settlement in _processPositionLocal()
.OracleVersion latestVersion
of Oracle.status()
may go backwards when updating to a new oracle provider and result in wrong settlement in _processPositionLocal()
.
This should be valid medium, even though not escalated. it('latest.timestamp moving back in time', async () => {
function setupOracle(price: string, timestamp : number, nextTimestamp : number) {
const oracleVersion = {
price: parse6decimal(price),
timestamp: timestamp,
valid: true,
}
oracle.at.whenCalledWith(oracleVersion.timestamp).returns(oracleVersion)
oracle.status.returns([oracleVersion, nextTimestamp])
oracle.request.returns()
}
var marketParameter = {
fundingFee: parse6decimal('0.1'),
interestFee: parse6decimal('0.1'),
oracleFee: parse6decimal('0.1'),
riskFee: parse6decimal('0.1'),
positionFee: parse6decimal('0.1'),
maxPendingGlobal: 5,
maxPendingLocal: 3,
settlementFee: parse6decimal('0'),
makerRewardRate: parse6decimal('0.0'),
longRewardRate: parse6decimal('0.0'),
shortRewardRate: parse6decimal('0.0'),
makerCloseAlways: false,
takerCloseAlways: false,
closed: false,
}
await market.connect(owner).updateParameter(marketParameter);
setupOracle('100', TIMESTAMP, TIMESTAMP + 100);
var collateral = parse6decimal('1000')
dsu.transferFrom.whenCalledWith(userB.address, market.address, collateral.mul(1e12)).returns(true)
await market.connect(userB).update(userB.address, parse6decimal('10.000'), 0, 0, collateral, false)
var collateral = parse6decimal('120')
dsu.transferFrom.whenCalledWith(user.address, market.address, collateral.mul(1e12)).returns(true)
await market.connect(user).update(user.address, 0, parse6decimal('1.000'), 0, collateral, false)
// open position
setupOracle('100', TIMESTAMP + 100, TIMESTAMP + 200);
await market.connect(user).update(user.address, 0, parse6decimal('1.000'), 0, 0, false)
var info = await market.locals(user.address);
var pos = await market.positions(user.address);
console.log("after open (price=100): user collateral = " + info.collateral + " long = " + pos.long);
// accumulate some pnl
setupOracle('90', TIMESTAMP + 200, TIMESTAMP + 300);
await market.connect(user).update(user.address, 0, parse6decimal('1.000'), 0, 0, false)
var info = await market.locals(user.address);
var pos = await market.positions(user.address);
var ver = await market.versions(TIMESTAMP + 200);
console.log("after settle pnl (price=90): user collateral = " + info.collateral + " long = " + pos.long + " ver_longValue: " + ver.longValue + " ver_makerValue: " + ver.makerValue);
// add collateral only
setupOracle('90', TIMESTAMP + 300, TIMESTAMP + 400);
dsu.transferFrom.whenCalledWith(userB.address, market.address, collateral.mul(1e12)).returns(true)
await market.connect(userB).update(userB.address, parse6decimal('10.000'), 0, 0, collateral, false)
// oracle.latest moves back in time
setupOracle('89', TIMESTAMP + 290, TIMESTAMP + 400);
await market.connect(user).update(user.address, 0, parse6decimal('1.000'), 0, 0, false)
var info = await market.locals(user.address);
var pos = await market.positions(user.address);
console.log("after move back in time (price=89): user collateral = " + info.collateral + " long = " + pos.long);
setupOracle('89', TIMESTAMP + 400, TIMESTAMP + 500);
await market.connect(user).update(user.address, 0, parse6decimal('1.000'), 0, 0, false)
setupOracle('89', TIMESTAMP + 500, TIMESTAMP + 600);
await market.connect(user).update(user.address, 0, parse6decimal('1.000'), 0, 0, false)
var info = await market.locals(user.address);
var pos = await market.positions(user.address);
console.log("User settled (price=89): collateral = " + info.collateral + " long = " + pos.long);
}) Console output:
|
Medium, not high, because:
|
Considering this issue a Unique Medium based on the above comments |
Looks like we missed this one since it was reopened last minute. After an initial review we think this is a false-positive, but please check our reasoning and let us know if you still see an issue. The actual potential bug here is an incorrect implementation of the So what we'd be looking for here for a valid bug is a specific case where an I ran through the case that I think you've outlined here (though I may have translate it incorrectly) and I don't see a way for it to bypass the Case
Assuming Let us know if there's an example here you can show us that does in fact bypass the |
From WatchPug, No PR attached. |
To cause these values, the following actions might happen: In such situations both checks in
|
Thanks @panprog, this is exactly what we needed 🙏 will get back on a fix. |
WATCHPUG
high
OracleVersion latestVersion
ofOracle.status()
may go backwards when updating to a new oracle provider and result in wrong settlement in_processPositionLocal()
.Summary
Vulnerability Detail
This is because when
Oracle.update(newProvider)
is called, there is no requirement thatnewProvider.latest().timestamp > oldProvider.latest().timestamp
.During the
processLocal
, encountering a non-existing version will result in using 0 as themakerValue
,longValue
, andshortValue
to settle PNL, causing the user's collateral to be deducted incorrectly.This is because L350 is skipped (as the global has been settled to a newer timestamp), and L356 enters the if branch.
PoC
Given:
latest().timestamp
of oracleProvider1 is 13:30When:
_versions[13:00]
in L337_versions[13:30]
in L353oracle.update(oracleProvider2)
(note: The currentlatest().timestamp
of oracleProvider2 is 13:20)market.update(account2) -> _settle()
, L350 is skipped; L35613:20 > 13:00
, enters_processPositionLocal()
:nextPosition.timestamp == 13:20
,version
is empty;context.local.accumulate
with emptyversion
will result in wrong PNL.Impact
Code Snippet
https://github.com/sherlock-audit/2023-07-perennial/blob/main/perennial-v2/packages/perennial-oracle/contracts/Oracle.sol#L106-L117
https://github.com/sherlock-audit/2023-07-perennial/blob/main/perennial-v2/packages/perennial/contracts/Market.sol#L327-L364
https://github.com/sherlock-audit/2023-07-perennial/blob/main/perennial-v2/packages/perennial/contracts/Market.sol#L390-L423
https://github.com/sherlock-audit/2023-07-perennial/blob/main/perennial-v2/packages/perennial/contracts/Market.sol#L430-L457
Tool used
Manual Review
Recommendation
Consider requireing
newProvider.latest().timestamp > oldProvider.latest().timestamp
inOracle.update(newProvider)
.The text was updated successfully, but these errors were encountered: