Platform: Code4rena
Start Date: 21/08/2023
Pot Size: $125,000 USDC
Total HM: 26
Participants: 189
Period: 16 days
Judge: GalloDaSballo
Total Solo HM: 3
Id: 278
League: ETH
Rank: 28/189
Findings: 5
Award: $719.15
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: said
Also found by: 0Kage, 0xCiphky, 0xkazim, 836541, AkshaySrivastav, Evo, HChang26, HHK, KrisApostolov, Neon2835, QiuhaoLi, Tendency, Toshii, bart1e, bin2chen, carrotsmuggler, chaduke, etherhood, gjaldon, glcanvas, josephdara, lanrebayode77, mahdikarimi, max10afternoon, nobody2018, peakbolt, qpzm, rvierdiiev, sces60107, tapir, ubermensch, volodya
17.313 USDC - $17.31
PerpetualVaultLP::deposit
computes shares for the deposit using the previewDeposit
function. previewDeposit
calculates shares proportional to the assets deposited v/s the totalCollateral
which is the sum of ETH
and RDPX
collateral.
Note that perpetualAtlanticVault.updateFunding()
is called AFTER shares are computed. updateFunding
has the effect of increasing totalCollateral
as the vault gets the funding fees
from the options it has written to RdpxV2Core
. Since total collateral used for calculating shares does not include the funding fees, depositors end up with marginally additional shares at the expense of existing LP's. In effect, this is like stealing value from existing LP's and sharing it with new depositors.
Pete got 4 additional shares at the expense of Bob because Pete's share calculation does not include the collateral already accrued in the form of funding fees.
Manual
Call perpetualAtlanticVault.updateFunding()
before calculating shares via previewDeposit(assets)
ERC4626
#0 - c4-pre-sort
2023-09-13T04:37:18Z
bytes032 marked the issue as duplicate of #867
#1 - c4-pre-sort
2023-09-13T04:37:27Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T19:56:35Z
GalloDaSballo changed the severity to 3 (High Risk)
#3 - c4-judge
2023-10-20T19:56:50Z
GalloDaSballo marked the issue as satisfactory
638.6354 USDC - $638.64
https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVault.sol#L333 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/core/RdpxV2Core.sol#L800 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVault.sol#L376
In the current implementation of PerpetualAtlanticVault::settle
, the only way an optionId
can be burnt is when the option is in-the-money. Note that at the time of bonding, a put option that is 25% out of the money is purchased by user. If the RDPX-ETH price is above this price, the option is rolled over to the next epoch and a new premium is again calculated inside the PerpetualAtlanticVault::calculateFunding
and added to the totalFundingForEpoch
.
Effectively, as long as the option is out of money, the funding cost continues to be deducted from core contract and passed to Atlantic vault LPs. There is no check if the underlying bond for which this put option is minted is redeemed or not.
Not completely sure if this is bug or feature of Perpetual Atlantic Puts but since the utility of these puts is to protect downside price movements of RDPX during bonding, such options should be retired once the underlying bond is redeemed by user. Paying out a premium on a perennial basis to Atlantic LP's does not make sense.
There are 2 implications:
optionStrikes[strike]
never reduces so long as the option continues to be OTM.Manual
Consider settling optionIds
that no longer have an underlying bonding. Put options for such bonds should expire and free up locked collateral in Atlantic Vaults.
Other
#0 - c4-pre-sort
2023-09-13T15:20:59Z
bytes032 marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-09-13T15:25:36Z
bytes032 marked the issue as primary issue
#2 - c4-sponsor
2023-09-25T18:58:09Z
psytama (sponsor) confirmed
#3 - c4-judge
2023-10-20T09:37:50Z
GalloDaSballo marked the issue as selected for report
#4 - 141345
2023-10-22T15:46:00Z
I believe this is invalid, because it is a feature of the protocol.
The option is always fully cash secured, and no way to redeem the underlying bond. And here the option is perpetual, it is by design to last if the option is OTM. And premium should be paid as long as it is not exercised, it is not lock of fund (since the put writer sell the options by themselves).
And the dups are also talking about the designed feature. This is not vuln, but the intended behavior.
#5 - GalloDaSballo
2023-10-25T09:15:32Z
I believe this is invalid, because it is a feature of the protocol.
The option is always fully cash secured, and no way to redeem the underlying bond. And here the option is perpetual, it is by design to last if the option is OTM. And premium should be paid as long as it is not exercised, it is not lock of fund (since the put writer sell the options by themselves).
And the dups are also talking about the designed feature. This is not vuln, but the intended behavior.
What happens when so much premiums are paid that the contract runs out of funds?
#6 - GalloDaSballo
2023-10-26T11:20:15Z
Allowing till EOD for clarifying the above by @141345
Unless I receive further info, I'm keeping this as open
#7 - 141345
2023-10-26T12:25:33Z
What happens when so much premiums are paid that the contract runs out of funds?
Yes, running out of funds is a problem, but it's on another level (protocol level). I think this issue seems more like feature improvement. As for the perpetual put option itself (product level), the mechanism is self-consistent, "perpetual premium" can be seen as designed feature (better to consider more situation, but not flawed).
The agreement of option counter parties is:
Here the term is, the buyer pay premium on and on, until settlement (forever premium if never hit strike price). The buyer (here is the protocol) need to make sure enough cash flow is generated in the long term. This agreement itself has no problem.
Some factory can rent some equipment customized for them, signs a lease with no expiry and agrees to pay forever.
But the situation that the factory going bankrupcy is possilbe, no money to pay, and the equipment is worthless to other people. That's not a bug of the original lease. But some situation nice to consider.
The lease term makes sense, it covers the situation if everyting goes as designed. Missing unexpected situation does not mean the lease term has loophole.
Back to this context, having no fund to pay is some problem on a higher level, the protocol itself has financial insolvency. When that happens, it's a question whether the protocol can continue to operate, not about the specific mechanism of the product.
That's why I think the designed behavior of "perpetual" is ok. Handle unexpected fund deficit is feature improvement.
#8 - ubermensch3dot0
2023-10-26T14:19:55Z
The put options are the mechanism to hedge the risk of a depeg when the rDPX price drops, therefore the premium paid for funding those options should always continue (OTM should not be settled). If not, you will have rDPX that's not backed by anything which exposes dpxETH to depegging which will theoretically drop the price down to 75% ETH price. Paying these premiums is what allow the protocol to have a way to sell the rDPX for ETH when its price drops to maintain the dpxETH stability. This hedging will have a lot of value when rDPX drops as it reduces the depeg risk and of course it will have a cost (premiums) if the rDPX do not drop, and that's how hedging works, limiting the risk for a cost. I suppose this is the main idea behind the whole protocol's implementation.
#9 - GalloDaSballo
2023-10-27T07:21:41Z
With the information
It's legitimate to ask whether this is a design decision, however the very real consequences of this are that funding is paid for something that users wouldn't want to, leading me to believe that this can be viewed as a footgun that is causing a leak of value under reasonable circumnstances
For this reason, I believe the finding is valid and of medium severity
🌟 Selected for report: bin2chen
Also found by: 0Kage, 0xDING99YA, QiuhaoLi, Toshii, Yanchuan, carrotsmuggler, deadrxsezzz, ether_sky, flacko, gjaldon, kutugu, mert_eren, pep7siup, qpzm, said, sces60107, tapir, ubermensch
39.433 USDC - $39.43
In line 273 of ReLPContract
, minTokenBAmount
is calculated as:
mintokenAAmount = (((amountB / 2) * tokenAInfo.tokenAPrice) / 1e8) - (((amountB / 2) * tokenAInfo.tokenAPrice * slippageTolerance) / 1e16);
In the above, tokenAInfo.tokenAPrice
is calculated in Line 221 as:
tokenAInfo.tokenAPrice = IRdpxEthOracle(addresses.rdpxOracle) .getRdpxPriceInEth();
To convert tokenB to tokenA, the inverse of tokenAPrice
needs to be used. Formula above would give a negligible value for mintokenAAmount
causing a huge slippage loss on tokenA (rDPX).
In the above case, tokenA corresponds to RDPX
and tokenB corresponds to WETH
. If we assume RDPX as $20 and ETH as $2000, rdpx:ETH = 0.01, ie 1 RDPX = 0.01 ETH. If amountB/2
is 1 ETH, then minTokenAAmount
gives a value of 0.01 instead of 100 (for simplicity, I assume slippageTolerance=0
). This would mean that this swap effectively has no slippage protection and bots can frontrun this transaction to cause significant slippage on RDPX.
Manual
Recommend the following changes:
uint256 tokenBPrice = IRdpxEthOracle(addresses.rdpxOracle) .getEthPriceInRdpx(); mintokenAAmount = (((amountB / 2) * tokenBPrice) / 1e8) - (((amountB / 2) * tokenBPrice * slippageTolerance) / 1e16);
Uniswap
#0 - c4-pre-sort
2023-09-09T05:26:56Z
bytes032 marked the issue as duplicate of #1805
#1 - c4-pre-sort
2023-09-11T06:59:44Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-16T08:47:53Z
GalloDaSballo changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-10-20T09:23:41Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: 0xTheC0der
Also found by: 0Kage, 0xDING99YA, 0xHelium, 0xbranded, 836541, ABA, Kow, QiuhaoLi, SpicyMeatball, T1MOH, __141345__, alexfilippov314, ayden, bart1e, bin2chen, chaduke, degensec, jasonxiale, joaovwfreire, nirlin, peakbolt, pep7siup, rvierdiiev, tnquanghuy0512
15.9268 USDC - $15.93
https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVault.sol#L240 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVault.sol#L568 https://github.com/code-423n4/2023-08-dopex/blob/eb4d4a201b3a75dd4bddc74a34e9c42c71d0d12f/contracts/perp-vault/PerpetualAtlanticVault.sol#L283
Documentation states that each epoch is 1 week long. Current contract has a provision to update funding duration via function call PerpetualAtlanticVault::updateFundingDuration
. However, even a small increase in funding duration can lead to excessively high premiums for put option buyers. This is due to a flaw in the way nextFundingPaymentTimestamp
is calculated. The PerpetualAtlanticVault::updateFundingDuration
implementation is as follows:
function nextFundingPaymentTimestamp() public view returns (uint256 timestamp) { return genesis + (latestFundingPaymentPointer * fundingDuration); }
Based on the actual value of latestFundingPaymentPointer
, the next funding payment timestamp could be way ahead into the future even if the funding duration is increased by a small amount. For eg., a funding duration increase from 7 days to 8 days after 100 epochs pushes the nextFundingPaymentTimestamp
100 days ahead.
Note that when a bonding process happens, a user is paying the premium in collateral token (ETH) in the PerpetualAtlanticVault::purchase
function. Time to expiry is calculated in line 283 as the next funding payment date minus current timestamp. Immediately after a minor increase in funding duration, the next put option buyer will buy an option with time to expiry as 100 days. Everything else being equal, as option duration increases, so does its premium. A user is paying for a 100 day option while his bonding process is just a few days long. This makes no sense and causes a heavy loss to the user participating in bonding process.
Price increase is non-linear and hence a long dated option will have much higher price than a regular epoch option. Since user is funding the premium, this is a direct loss to user.
latestFundingDurationPointer = 100
)Manual
nextFundingPaymentTimestamp
calculation needs to be changed. Keep a storage variable previousFundingPaymentTimestamp
that gets updated every time an epoch gets completed or in the updateFunding
. This simply adds the funding duration to the previousFundingPaymentTimestamp
to get the nextFundingPaymentTimestamp
Current implementation risks big jumps and unexpected changes even with minor change in fundingDuration
Math
#0 - c4-pre-sort
2023-09-08T06:32:37Z
bytes032 marked the issue as duplicate of #980
#1 - c4-pre-sort
2023-09-11T08:23:45Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T11:09:32Z
GalloDaSballo changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-10-20T11:12:05Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: Vagner
Also found by: 0Kage, 0xCiphky, 0xnev, ABAIKUNANBAEV, Aymen0909, Evo, KmanOfficial, MohammedRizwan, T1MOH, Viktor_Cortess, Yanchuan, ak1, alexzoid, bin2chen, codegpt, hals, ladboy233, mrudenko, nemveer, oakcobalt, peakbolt, pep7siup, qbs, said, savi0ur, tapir, wintermute, zaevlad, zzebra83
7.8372 USDC - $7.84
UniV2LiquidityAMO
is used for market operations that maintain the DpxETH:ETH
peg. Whenever liquidity is added, removed or DpxETH is swapped for ETH or vice versa, the residual tokens are sent back to the RpdxV2Core
contract using the UniV2LiquidityAMO::_sendTokensToRdpxV2Core
. However, this function fails to sync the accounting balances with token balances by triggering the RpdxV2Core::sync
function. This effectively means the token balances from market operations are not correctly updated in the core contract.
Note that, the UniV3LiquidityAMO
does the syncing after the transfers
Not updating token balances
RpdxV2Core::getReserveTokenInfo
is calledtokenBalance
can cause underflow in RpdxV2Core::_purchaseOptions
, RpdxV2Core::stake
or ``RpdxV2Core::settle` functionsProvide direct links to all referenced code in GitHub. Add screenshots, logs, or any other relevant proof that illustrates the concept.
Manual
Call IRdpxV2Core(rdpxV2Core).sync()
after making all the transfers inside the UniV2LiquidityAMO::_sendTokensToRdpxV2Core
function
Under/Overflow
#0 - c4-pre-sort
2023-09-09T03:58:57Z
bytes032 marked the issue as duplicate of #798
#1 - c4-pre-sort
2023-09-09T04:09:34Z
bytes032 marked the issue as duplicate of #269
#2 - c4-pre-sort
2023-09-11T11:58:43Z
bytes032 marked the issue as sufficient quality report
#3 - c4-judge
2023-10-15T18:11:39Z
GalloDaSballo marked the issue as satisfactory