-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
0xbranded - Funding Paid != Funding Received #83
Comments
Escalate Issue should be invalidated. It does not showcase any Medium-severity impact, but rather just a slightly incorrect code comment. Contest readme did not include said invariant as one that must always be true, so simply breaking it slightly does not warrant Medium severity. |
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 included code comment was an explicit invariant outlined by the team:
The problematic code was actually intended to enforce this invariant, however it does so incorrectly. In the case of a negative position due to fee underpayment, Not only that, but users may have paid their full funding payment and the This specification is the source of truth, and the code clearly does not abide by it. The issue proposes alternative methods for accounting funding fee payments to ensure this invariant consistently holds. |
Also I do think this issue is similar to #18 and #93 Will leave it to HoJ if they are combined since the source of the error is the same, but different impacts are described. This issue discusses the code being not to spec and breaking an invariant. |
@spacegliderrrr is correct that indeed breaking the invariant from the code comment doesn't make the issue medium-severity necessarily. But, as I understand, it also works as intended. The position pays its fees, but if there is not enough collateral, then position cannot pay the fee and this fee will be unpaid/underpaid. And, IIUC, in this protocol the borrowing fees comes first and if the remaining collateral cannot pay the funding fee in full, then it shouldn't be. Hence, this is also enough of a contextual evidence that the comment is outdated. Do I miss anything here? |
@WangSecurity Most of your points are correct. Yes the code works as expected. Funding fee comes before borrowing fee. But for both of them, since due to fail to liquidate position on time, fees can exceed the total position collateral. Because of this, funding fees are paid with priority and if there's anything left, borrowing fees are paid for as much as there's left. In the case where funding fees are overpaid (for more than the total position collateral), the other side receives these fees in a FIFO order, which is also clearly stated in the comments. |
@spacegliderrrr is correct, funding fees are paid before borrow fees. As such there is no evidence that the comment spec is outdated, nor does any other documentation such as the readme appear to indicate so.
To elaborate on this point, the vulnerability describes the case that Regardless, this seems to be a clearly defined invariant. Not only from this comment but further implied by the logic of this fee handling, which penalizes negative positions to build up some "insurance" tokens to hedge against the case that funding fees are underpaid. It also intuitively makes sense for this invariant to generally hold as otherwise malfunctions can occur such as failure to close positions; several linked issues reported various related problems stemming from this invariant breaking as well. |
Another question I've got after re-reading the report. The impact section says there will be stuck tokens in the contract which will never be paid. But, as I understand the problem is different. The issue is that these tokens are underpaid, e.g. if the funding fee is 10 tokens, but the remaining collateral is only 8, then there are 2 tokens underpaid. So, how the are stuck tokens in the contract, if the funding fee is not paid in full. Or it refers to the funding_received being larger than the funding_received actually? |
Since no answer is provided, I assume it's a typo in the report and the contract doesn't have fewer tokens, since the fee is underpaid, not overpaid. |
Apologies for the delayed response @WangSecurity Yes, the original statement was as intended. That portion of the report was pointing out that there are two problematic impacts of the current approach that cause
The outlined invariant is broken which results in loss of funds / broken functionality. It will prevent closing of some positions in the former case, and will result in some funds being stuck in the latter case. The current approach of penalizing negative positions is intended to 'build up an insurance' as stated by the team. But as mentioned here, not only does it not remediate the issue it actually furthers the imbalance. The funding tokens have already been underpaid in the case of a negative position - preventing those positions from receiving their fees results in underpayment of their funding as well. |
Could you elaborate on this? Similar to my analysis here, If the position is negative, but the collateral is not 0, the funding paid is larger than the position's collateral, the funding paid will be decreased to pos. collateral. The funding received will be 0 (if collateral is < funding paid, c1: remaining =0 and deducted =pos.collateral. Then c2: remaining =0 and deducted =0. funding received =0, because remaining =0. So, not sure how funding_received can be > funding paid and I need a more concrete example with numbers.
Agree that it's possible.
However, the report doesn't showcase that this broken invariant (when funding received < funding paid) will result in any of this. The report only says that it will lead to a dilution of fees, but there is still no explanation of how. Hence, without a concrete example of how this leads to positions not being closed, thus leading to losses of these positions, and owners, I cannot verify. I see that the invariant doesn't hold, but this is not sufficient for Medium severity (only broken invariants from README are assigned Medium, not invariants from code comments). Hence, the decision for now is to invalidate this issue and accept the escalation. But, if there's a POC, how does this lead to loss of funds in case funding received < funding paid, or how does funding received > funding paid and how does this lead to a loss of funds, I will consider changing my decision. |
If no answer is provided, planning to accept the escalation and invalidate the issue. |
Doesn't the below rule from the severity categorization apply here? That's what I had in mind when creating the issue, since this comment was the only source of documentation on this specification. There is no indication that this is out of date based on any other specs or code elsewhere.
Nevertheless there are loss of funds from each case, let me first prove that
This is the crux of the issue I am reporting here. That's true in the case of a single position, the issue is that While that positions paid_long_term : uint256 = self.apply(fs.long_collateral, fs.funding_long * new_terms)
received_short_term : uint256 = self.divide(paid_long_term, fs.short_collateral) This is because this collateral is not excluded from Now the issue causing loss of funds on this side is that positions will fail to close. On the other side, where In this case, the balance Upholding this balance of funding payments to receipts is an important invariant which causes loss of funds and protocol disruptions as outlined above. This is even acknowledged by the team, since this current approach is meant to build up an "insurance" by penalizing negative positions to pay out future negative positions. What it fails to acknowledge is that the penalized position has already underpaid its funding fee (in token A), and now the balance is further distorted by eliminating its funding payment (in token B). To move closer to equilibrium between the two sides of funding, a direct approach such as socializing fee underpayments is recommended instead. There are a few different moving parts so let me know if any component of this line of reasoning is unclear and I could provide specific examples if needed. |
I didn't say the rule applies here, but if there's something said in the code comments and the code doesn't follow it, the issue still has to have at least Medium severity to be valid:
So if the issue breaks an invariant from code comments, but it doesn't have Medium severity, then it's invalid.
That still doesn't prove how Hence, this calculation of funding paid and received will be made only when closing or liquidating the position. So, the following is wrong:
It won't accrue because the position is either already liquidated or closed. Hence, the scenario of About the So if the position didn't manage to pay the full Hence, my decision remains: accept the escalation and invalidate the issue. If you still see that I'm wrong somewhere, you're welcome to correct me. But, to make it easier, provide links to the appropriate LOC you refer to. |
That's not quite correct. Yes, def calc(id: uint256, long: bool, collateral: uint256, opened_at: uint256) -> SumFees:
period: Period = self.query(id, opened_at)
P_b : uint256 = self.apply(collateral, period.borrowing_long) if long else (
self.apply(collateral, period.borrowing_short) )
P_f : uint256 = self.apply(collateral, period.funding_long) if long else (
self.apply(collateral, period.funding_short) )
R_f : uint256 = self.multiply(collateral, period.received_long) if long else (
self.multiply(collateral, period.received_short) )
return SumFees({funding_paid: P_f, funding_received: R_f, borrowing_paid: P_b}) The terms
The point of this issue is that user's will not receive those lost funds. Since the global The current approach is problematic with loss of funds and disrupted liquidation functionality. There are more direct ways to achieve the correct balance of funding payments to receipts. |
To finally confirm, the problem here is that these funds are just locked in the contract, and no one can get them, correct? |
as I got the main point in this report is funding_received can exceed funding_paid but this isn't correct
provided PoC def test_funding_received_cannot_exceed_than_funding_paid(core, BTC,mint_token, STX, lp_provider, LP, open, mint, short, positions, owner, pools, fees, oracle, api, long, close):
pools.CORE() == core
fees.CORE() == core
positions.CORE() == core
oracle.API() == api
core.API() == api
mint_token(BTC, btc(1000), lp_provider)
mint_token(BTC, btc(1000), short)
mint_token(STX, d(100000), long)
mint_token(STX, d(100_000), lp_provider)
assert BTC.balanceOf(lp_provider) == btc(1000)
assert BTC.balanceOf(short) == btc(1000)
assert STX.balanceOf(lp_provider) == d(100_000)
core.fresh("BTC-STX", BTC, STX, LP, sender=owner)
BTC.approve(core.address, btc(1000), sender=lp_provider)
STX.approve(core.address, d(100_000), sender=lp_provider)
mint(BTC, STX, LP, btc(100), d(100_000), price=d(50_000), sender=lp_provider)
BTC.approve(core.address, 10000000, sender=short)
STX.approve(core.address, d(100000), sender=long)
print("stx_balance:", STX.balanceOf(long) / 1e6)
open(BTC, STX, True, d(50000), 1, price=d(50_000), sender=long)
open(BTC, STX, False, 10000, 1, price=d(50_000), sender=short)
chain.mine(20000)
position = positions.value(1, ctx(d(50_000)))#funding_fee payer
position2 = positions.value(2, ctx(d(50_000)))#funding_fee receiver
print("position.remaining_collateral:", position.pnl.remaining / 1e6)
print("position.fees.funding_paid:", position.fees.funding_paid / 1e6)
print("position.funding_received:", position2.fees.funding_received / 1e6)
#@>>>>>position.remaining_collateral: 0.0
#@>>>>>position.fees.funding_paid: 50000.0
#@>>>>>position.funding_received: 50000.0 |
Yeah, there isn't a proof it can happen, the problem we are discussing now is that in cases where funding received is larger than funding paid (can happen when the position is negative), the funding received would be stuck in the contract with no one being able to get them. @rickkk137 would the funding fees be distributed to other users in this case? |
In my poc the position finally become negative but funding received is equal to funding paid |
Thank you for this correction. With this, the escalation will be accepted and the issue will be invalidated. The decision will be applied in a couple of hours. |
Hi @rickkk137 can you specifically clarify the scenario which the PoC is describing? Because from my understanding, it is not showing the case which I described. The nuance is subtle, allow me to explain Let's say there is one position on the long side, two on the short side, and that longs are paying shorts. Now if one of the short positions goes negative, the other will not receive the total funding fees over the period, it will only get its pro-rata share from the time that the (now liquidated) position was still active. For comparison, it seems your PoC is showing a single long and short, and you are showing that when the long is liquidated the short should still receive their funding payment. This is a completely different scenario from what is described in this issue. To answer your earlier question @WangSecurity
Yes, thats correct. That portion of the funding payments is not accessible to any users. |
when a position be penalized and funding_received for that position become zero and base or quote collateral's pool will be decrease
its mean other position get more funding_received compared to past because funding_recieved has reserve relation with base or qoute collateral
|
The pro rata share of funding received will be correctly adjusted moving forward from liquidation. But the point is that the period.funding_received terms already included the now liquidated collateral, so the other positions do not receive adjusted distributions to account for that. |
|
That's not quite right. From paid_short_term : uint256 = self.apply(fs.short_collateral, fs.funding_short * new_terms)
received_long_term : uint256 = self.divide(paid_short_term, fs.long_collateral)
received_long_sum : uint256 = self.extend(fs.received_long_sum, received_long_term, 1) So as mentioned in my point above, the collateral at the time of each global fee update is used. When a user later claims his pro-rate share, he will receive his fraction of the total collateral at the time of the fee update. Fee updates are performed continuously after each operation, and the total collateral may no longer be representative due to liquidated positions being removed from this sum. However, they were still included in the Thus some share of these global fees are not accessible. |
As I understand it:
So even though the closed/liquidated position didn't receive any fees and they should go to another short, the Hence, when the other short position gets the fees, they will add only a portion from that period, not the full fee. Thus, I agree it should remain valid. However, medium severity should be kept because, in reality, if this situation occurs, the collateral of that closing/liquidatable position would be quite low (lower than |
Result: |
Escalations have been resolved successfully! Escalation status:
|
0xbranded
Medium
Funding Paid != Funding Received
Summary
Due to special requirements around receiving funding fees for a position, the funding fees received can be less than that paid. These funding fee payments are still payed, but a portion of them will not be withdrawn, and become stuck funds. This also violates the contract specification that
sum(funding_received) = sum(funding_paid)
.Vulnerability Detail
In
calc_fees
there are two special conditions that impact a position's receipt of funding payments:If the position has run out of collateral by the time it is being closed, he will receive none of his share of funding payments. Additionally, if the available collateral is not high enough to service the funding fee receipt, he will receive only the greatest amount that is available.
These funding fee payments are still always made (deducted from remaining collateral), whether they are received or not:
When a position is closed under most circumstances, the pool will have enough collateral to service the corresponding fee payment:
When positions are closed, the original collateral (which was placed into the pool upon opening) is removed. However, the amount of funding payments a position made is added to the pool for later receipt. Thus, when positions are still open there is enough position collateral to fulfill the funding fee payment and when they close the funding payment made by that position still remains in the pool.
Only when the amount of funding a position paid exceeded its original collateral, will there will not be enough collateral to service the receipt of funding fees, as alluded to in the comments. However, it's possible for users to pay the full funding fee, but if the borrow fee exceeds the collateral balance remaining thereafter, they will not receive any funding fees. As a result, it's possible for funding fees to be paid which are never received.
Further, even in the case of funding fee underpayment, setting the funding fee received to 0 does not remedy this issue. The funding fees which he underpaid were in a differing token from those which he would receive, so this only furthers the imbalance of fees received to paid.
Impact
core.vy
includes a specification for one of the invariants of the protocol:This invariant is clearly broken as some portion of paid funding fees will not be received under all circumstances, so code is not to spec. This will also lead to some stuck funds, as a portion of the paid funding fees will never be deducted from the collateral. This can in turn lead to dilution of fees for future funding fee recipients, as the payments will be distributed evenly to the entire collateral including these stuck funds which will never be removed.
Code Snippet
Tool used
Manual Review
Recommendation
Consider an alternative method of accounting for funding fees, as there are many cases under the current accounting where fees received/paid can fall out of sync.
For example, include a new state variable that explicitly tracks unpaid funding fee payments and perform some pro rata or market adjustment to future funding fee recipients, specifically for that token.
The text was updated successfully, but these errors were encountered: