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: 17/189
Findings: 4
Award: $1,067.33
🌟 Selected for report: 0
🚀 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
First transaction in block might receive more shares than the same transaction at the second position. This behavior is un-honest, and may entail reputational losses in combination with the current implementation.
Let's consider deposit
:
function deposit( uint256 assets, address receiver ) public virtual returns (uint256 shares) { // Check for rounding error since we round down in previewDeposit. require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); perpetualAtlanticVault.updateFunding(); // Need to transfer before minting or ERC777s could reenter. collateral.transferFrom(msg.sender, address(this), assets); ... }
Calling perpetualAtlanticVault.updateFunding();
might increase _totalCollateral
amount, and because shares
amount depends on _totalCollateral
and totalSupply
i.e.: shares = amount * supply / collateral
.
So, lets consider next case:
Current supply = 100, collateral = 100
updateFunding
, now supply = 100, collateral = 150
;supply = 200, collateral = 250
shares = 100 * 200 / 250 = 80
, so user here receive not the same proportional amount of shares as first one.Honest behavior only possible if updateFunding
will be called before shares calculated.
Manual review
Move line perpetualAtlanticVault.updateFunding();
at the beginning of the deposit
.
Other
#0 - c4-pre-sort
2023-09-07T13:44:25Z
bytes032 marked the issue as duplicate of #867
#1 - c4-pre-sort
2023-09-11T09:06:17Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T19:56:34Z
GalloDaSballo changed the severity to 3 (High Risk)
#3 - c4-judge
2023-10-20T19:56:54Z
GalloDaSballo marked the issue as satisfactory
🌟 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
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L118-L135 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L145-L175 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L218-L235 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVaultLP.sol#L274-L284 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L502-L524 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L462-L496
An attacker can use flash loan and withdraw significant part of funding intended for collateral providers.
Let's consider code snippet from deposit
:
require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); perpetualAtlanticVault.updateFunding(); collateral.transferFrom(msg.sender, address(this), assets); _mint(receiver, shares); _totalCollateral += assets;
from redeem
:
(assets, rdpxAmount) = redeemPreview(shares); require(assets != 0, "ZERO_ASSETS"); _rdpxCollateral -= rdpxAmount; beforeWithdraw(assets, shares); _burn(owner, shares); collateral.transfer(receiver, assets); IERC20WithBurn(rdpx).safeTransfer(receiver, rdpxAmount);
Next consider convertToShares
:
uint256 supply = totalSupply; uint256 rdpxPriceInAlphaToken = perpetualAtlanticVault.getUnderlyingPrice(); uint256 totalVaultCollateral = totalCollateral() + ((_rdpxCollateral * rdpxPriceInAlphaToken) / 1e8); return supply == 0 ? assets : assets.mulDivDown(supply, totalVaultCollateral);
And convertToAssets
:
uint256 supply = totalSupply; return (supply == 0) ? (shares, 0) : ( shares.mulDivDown(totalCollateral(), supply), shares.mulDivDown(_rdpxCollateral, supply) );
in the other words, if we simplify formulas above:
shares = asserts * supply / collateral
asserts = shares * collateral / supply
This implementation gives us possibility to withdraw significant amount of funding from VaultLP
contract use flash loan.
Possible case:
Vault
to VaultLP
(via Vault.updateFunding()
);deposit
it in VaultLP
, let's assume before this transaction: supply = 100, collateral = 100
;shares = 50 * 100 / 100 = 50
;Vault.updateFunding()
called, and it updates collateral
from 100
to 120
;supply = 150, collateral = 170
, hacker has 50 shares;redeem
, and his amount to withdraw will be: amount = 50 * 170 / 150 = 56.6
;This can be fixed if we move updateFunding
at the beginning of the deposit
function:
50 eth
, but now supply = 100, collateral = 120 => shares = 41.6
;41.6 * 170 / 141.6 = 50 eth
;Manual review
In function deposit
move line perpetualAtlanticVault.updateFunding();
to the first place.
MEV
#0 - c4-pre-sort
2023-09-09T11:01:20Z
bytes032 marked the issue as duplicate of #867
#1 - c4-pre-sort
2023-09-11T09:08:18Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T19:18:18Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: 0xrafaelnicolau
Also found by: 0x111, 0xCiphky, 0xMosh, 0xWaitress, 0xc0ffEE, 0xkazim, 0xnev, 0xvj, ABAIKUNANBAEV, Aymen0909, Baki, ElCid, HChang26, HHK, Inspex, Jorgect, Kow, Krace, KrisApostolov, LFGSecurity, MiniGlome, Nyx, QiuhaoLi, RED-LOTUS-REACH, Talfao, Toshii, Vagner, Viktor_Cortess, Yanchuan, _eperezok, asui, atrixs6, bart1e, bin2chen, carrotsmuggler, chaduke, chainsnake, deadrxsezzz, degensec, dethera, dimulski, dirk_y, ether_sky, gizzy, glcanvas, grearlake, gumgumzum, halden, hals, kodyvim, koo, ladboy233, lanrebayode77, max10afternoon, minhtrng, mussucal, nobody2018, peakbolt, pontifex, qbs, ravikiranweb3, rvierdiiev, said, tapir, ubermensch, volodya, wintermute, yashar, zaevlad, zzebra83
0.0734 USDC - $0.07
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L975-L990 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L995-L1008
Contract state might become in incorrect state. Users funds will be locked, and only emergencyWithdraw
can save the situation. But still -- contract will be in non working state. Protocol can lose their users and confidence.
Let's consider withdraw
function:
function withdraw( uint256 delegateId ) external returns (uint256 amountWithdrawn) { _whenNotPaused(); _validate(delegateId < delegates.length, 14); Delegate storage delegate = delegates[delegateId]; _validate(delegate.owner == msg.sender, 9); amountWithdrawn = delegate.amount - delegate.activeCollateral; _validate(amountWithdrawn > 0, 15); delegate.amount = delegate.activeCollateral; IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn); emit LogDelegateWithdraw(delegateId, amountWithdrawn); }
Delegator can withdraw all unused amount:
amountWithdrawn = delegate.amount - delegate.activeCollateral; _validate(amountWithdrawn > 0, 15); delegate.amount = delegate.activeCollateral;
but we additionally handle totalWethDelegated
which is responsible for amount of total delegated tokens.
When user withdraws his delegate amount, the contract must update totalWethDelegated
, but this doesn't happened.
This lead to:
totalWethDelegated
amount;sync
function;Possible case to Stop the system:
sync()
function, and now balance will be decreased to 1 eth. This will be disaster because users may lost their money, and contract state will be incorrect.Manual review
When you're doing withdraw decease totalWethDelegated
.
DoS
#0 - c4-pre-sort
2023-09-07T07:41:14Z
bytes032 marked the issue as duplicate of #2186
#1 - c4-pre-sort
2023-09-07T07:41:20Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T17:53:15Z
GalloDaSballo marked the issue as satisfactory
#3 - c4-judge
2023-10-20T17:55:32Z
GalloDaSballo changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-10-21T07:38:55Z
GalloDaSballo changed the severity to 3 (High Risk)
#5 - c4-judge
2023-10-21T07:42:47Z
GalloDaSballo marked the issue as partial-50
909.7371 USDC - $909.74
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/decaying-bonds/RdpxDecayingBonds.sol#L19-L21 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/decaying-bonds/RdpxDecayingBonds.sol#L162-L170 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/decaying-bonds/RdpxDecayingBonds.sol#L157 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/decaying-bonds/RdpxDecayingBonds.sol#L41
Decaying bonds was introduced to encourage those who received a loss in the protocol, and how it was intended -- these bonds can be used by harmed users only.
But in current implementation discount can be used by anyone, not only by harmed user. Because initial bonds owner can transfer these bonds like plain old NFT token.
This leads to additional profit for those who do not deserve a discount.
Let's consider bond:
struct Bond { address owner; uint256 expiry; uint256 rdpxAmount; }
here owner field which filled in mint
function:
function mint( address to, uint256 expiry, uint256 rdpxAmount ) external onlyRole(MINTER_ROLE) { _whenNotPaused(); require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter"); uint256 bondId = _mintToken(to); bonds[bondId] = Bond(to, expiry, rdpxAmount); emit BondMinted(to, bondId, expiry, rdpxAmount); }
Bond get his expiry
, amount
and owner
, and storages in bonds
array, and token was written to ERC721 layout too.
In core contract owner checks like NFT owner not field in Bond
structure.
_validate( IRdpxDecayingBonds(addresses.rdpxDecayingBonds).ownerOf(_bondId) == msg.sender, 9 );
That's incorrect, because owner might be changed with transferFrom
, which is implemented in ERC721 contract.
So, possible case when Bob got his decaying bond, then transfer this bond to Alice, and Alice uses this discount.
Manual audit
Either Forbid to transfer bonds, or remove field owner
from
struct Bond { address owner; uint256 expiry; uint256 rdpxAmount; }
ERC721
#0 - c4-pre-sort
2023-09-09T17:25:55Z
bytes032 marked the issue as duplicate of #532
#1 - c4-pre-sort
2023-09-15T09:34:36Z
bytes032 marked the issue as duplicate of #1030
#2 - c4-judge
2023-10-18T12:19:32Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: juancito
Also found by: 0xDING99YA, 0xTiwa, 0xkazim, 0xnev, ABA, ArmedGoose, Aymen0909, Bauchibred, Evo, IceBear, KrisApostolov, MohammedRizwan, Nikki, QiuhaoLi, T1MOH, Toshii, WoolCentaur, Yanchuan, __141345__, asui, bart1e, carrotsmuggler, catellatech, chaduke, codegpt, deadrxsezzz, degensec, dethera, dirk_y, erebus, ether_sky, gjaldon, glcanvas, jasonxiale, josephdara, klau5, kodyvim, ladboy233, lsaudit, minhquanym, parsely, peakbolt, pep7siup, rvierdiiev, said, savi0ur, sces60107, tapir, ubermensch, volodya, zzebra83
140.2087 USDC - $140.21
links to typos: https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L84 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L117
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L816 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L240 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L117 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L99 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L60 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L52
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/reLP/ReLPContract.sol#L75
Replace all occurrences of constant like 1e10
with ONE_HUNDRED_PERCENT
List:
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L658
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L658
1e10
with ONE_HUNDRED_PERCENT
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L669
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L673
1e8
with ONE_TOKEN
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L947
100e8
with ONE_HUNDRED_PERCENT
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L949
1e16
with MILI_ETHER
And in all other files the same.
From now, we have different implementations of emergencyWithdraw
function emergencyWithdraw( address[] calldata tokens ) external onlyRole(DEFAULT_ADMIN_ROLE) { IERC20WithBurn token; for (uint256 i = 0; i < tokens.length; i++) { token = IERC20WithBurn(tokens[i]); token.safeTransfer(msg.sender, token.balanceOf(address(this))); } emit LogEmergencyWithdraw(msg.sender, tokens); }
function emergencyWithdraw( address[] calldata tokens ) external onlyRole(DEFAULT_ADMIN_ROLE) { _whenPaused(); IERC20WithBurn token; for (uint256 i = 0; i < tokens.length; i++) { token = IERC20WithBurn(tokens[i]); token.safeTransfer(msg.sender, token.balanceOf(address(this))); } emit EmergencyWithdraw(msg.sender, tokens); }
function emergencyWithdraw( address[] calldata tokens, bool transferNative, address payable to, uint256 amount, uint256 gas ) external onlyRole(DEFAULT_ADMIN_ROLE) { _whenPaused(); if (transferNative) { (bool success, ) = to.call{ value: amount, gas: gas }(""); require(success, "RdpxReserve: transfer failed"); } IERC20WithBurn token; for (uint256 i = 0; i < tokens.length; i++) { token = IERC20WithBurn(tokens[i]); token.safeTransfer(msg.sender, token.balanceOf(address(this))); } }
These functions must be the same.
Make single contract EmegencyWithdraw.sol
and implement all your contract with inheritance from this contract.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/dpxETH/DpxEthToken.sol#L25 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/dpxETH/DpxEthToken.sol#L26
DpxEthToken
minted/burned by Core contract, and here msg.sender
get MINTER
and BURNER
role,
which must be reassigned latte.
At the first creator can mint multiple tokens to any address before transfer role.
At the second creator can give role to another address.
This is bad solution. Better use approach like in UniswapV3 (I described it in analysis report).
If user has a lot of bonds there might be a problem to receive all list of items. Due to limit of external nodes like Infura.
Better use pagable pattern -- i.e. request only subpart of bonds.
bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
This role created, but not assigned.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L512 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L516 https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L521
This: (currentFundingRate * (block.timestamp - startTime)) / 1e18
might be assigned to variable.
This improves readability and saves gas.
lastUpdateTime == 0
never will be 0 because it's moved anyway in updateFundingPaymentPointer();
so rewrite this code to:
uint256 startTime = lastUpdateTime;
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L240 addAssetTotokenReserves -> addAssetToTokenReserves
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L270 removeAssetFromtokenReserves -> removeAssetFromTokenReserves
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L64
amoAddresses only filled, but not used anyway.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L410
Due to unclear error code and bad design -- developers created a lot of mistakes while create validation code
_validate(_amount > 0, 17);
error code shouldn't be 17, because: E17: "Zero address"
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L652
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L624-L685
calculateBondCost
;ONE_HUNDRED_PERSENT
instead of 1e10;(_rdpxAmount * rdpxBurnPercentage) / 1e10
DEFAULT_PRECISION
instead of 1e8.https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/perp-vault/PerpetualAtlanticVault.sol#L181-L212
// Transfer rDPX and ETH token from user
You already transferred ETH, so rewrite to:
// Transfer rDPX token from user
Current implementation might fail if any address will be changed twice due to safeApprove
implementation
collateralToken.safeApprove( addresses.perpetualAtlanticVaultLP, type(uint256).max );
see:
function safeApprove(IERC20 token, address spender, uint256 value) internal { // safeApprove should only be called when setting an initial allowance, // or when resetting it to zero. To increase and decrease it, use // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' require( (value == 0) || (token.allowance(address(this), spender) == 0), "SafeERC20: approve from non-zero to non-zero allowance" ); _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); }
This can be handled by:
function setLpAllowance(bool increase) external onlyRole(DEFAULT_ADMIN_ROLE) { increase ? collateralToken.approve( addresses.perpetualAtlanticVaultLP, type(uint256).max ) : collateralToken.approve(addresses.perpetualAtlanticVaultLP, 0); }
But this is not convenient, due to a lot of additional actions.
Better separate setAddresses
by multiple atomic setters like:
setOptionPricing setAssetPriceOracle ...
Current implementation handles ATM options, but your doc told that you don't settle ATM options https://docs.dopex.io/option-fundamentals/option-settlement
More-over here not any meaning to handle it, because you got same amount of funds but in another tokens.
So, forbid to settle ATM option, and implement option settlement as was described in my previous report.
https://github.com/code-423n4/2023-08-dopex/blob/main/contracts/core/RdpxV2Core.sol#L70
string[] public reserveTokens;
Actually, they are symbols, not tokens.
#0 - c4-pre-sort
2023-09-10T11:46:17Z
bytes032 marked the issue as sufficient quality report
#1 - GalloDaSballo
2023-10-10T11:47:14Z
Fix Typos OOS Make constants instead of raw values OOS Unify withdraw functions R Don't assign role on msg.sender I, they can rescind it User might have a lot of decaying bonds OOS Unused role OOS Copy-paste R Unreached code NC wrong function naming NC Unused array R Incorrect error params L Incorrect comment NC Improve _transfer R Set addresses might fail L Not needed to handle ATM options L Rename variable to symbols NC
#2 - c4-judge
2023-10-20T10:22:01Z
GalloDaSballo marked the issue as grade-a