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: 46/189
Findings: 8
Award: $364.42
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: klau5
Also found by: 0x3b, 0xCiphky, 0xDING99YA, 0xWaitress, 0xbranded, 0xc0ffEE, 0xklh, 0xsurena, 0xvj, ABA, AkshaySrivastav, Anirruth, Aymen0909, Baki, Blockian, BugzyVonBuggernaut, DanielArmstrong, Evo, GangsOfBrahmin, HChang26, Inspex, Jiamin, Juntao, Kow, Krace, KrisApostolov, LFGSecurity, LokiThe5th, Mike_Bello90, Norah, Nyx, QiuhaoLi, RED-LOTUS-REACH, SBSecurity, Snow24, SpicyMeatball, T1MOH, Tendency, Toshii, Udsen, Yanchuan, __141345__, ak1, asui, auditsea, ayden, bart1e, bin2chen, blutorque, carrotsmuggler, chaduke, chainsnake, circlelooper, clash, codegpt, crunch, degensec, dirk_y, ge6a, gjaldon, grearlake, jasonxiale, juancito, ke1caM, kodyvim, kutugu, ladboy233, lanrebayode77, mahdikarimi, max10afternoon, mert_eren, nirlin, nobody2018, oakcobalt, parsely, peakbolt, pks_, pontifex, ravikiranweb3, rokinot, rvierdiiev, said, savi0ur, sces60107, sh1v, sl1, spidy730, tapir, tnquanghuy0512, ubermensch, visualbits, volodya, wintermute
0.0098 USDC - $0.01
PerpetualAtlanticVaultLP.subtractLoss()
Use ==
to determine if the current contract balance is correct, so that if a malicious user donates 1 wei
into the contract
will cause this check to always fail, and subtractLoss()
will always fail
PerpetualAtlanticVaultLP.subtractLoss()
Use ==
to determine if the current contract balance is correct
function subtractLoss(uint256 loss) public onlyPerpVault { require( @> collateral.balanceOf(address(this)) == _totalCollateral - loss, "Not enough collateral was sent out" ); _totalCollateral -= loss; }
If a malicious user donates 1 wei
to the vaultLP contract, this will cause this check to always fail, and subtractLoss()
will always fail.
It makes sense to just make sure that the current balance is greater than or equal to
The following code demonstrates that donating 1 wei
to vaultLP
will cause the check to always fail, subtractLoss()
will always fail.revert Not enough collateral was sent out
add to perp-vault/Unit.t.sol
function testSettleRevert() public { weth.mint(address(1), 1 ether); deposit(1 ether, address(1)); vault.purchase(1 ether, address(this)); uint256[] memory ids = new uint256[](1); ids[0] = 0; priceOracle.updateRdpxPrice(0.010 gwei); // ITM //donate 1 wei to vaultLp weth.mint(address(vaultLp), 1 wei); // settle will revert vault.settle(ids); }
$ forge test --match-test testSettleRevert Running 1 test for tests/perp-vault/Unit.t.sol:Unit [FAIL. Reason: Not enough collateral was sent out] testSettleRevert() (gas: 765154) Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 2.22s Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests) Failing tests: Encountered 1 failing test in tests/perp-vault/Unit.t.sol:Unit [FAIL. Reason: Not enough collateral was sent out] testSettleRevert() (gas: 765154)
function subtractLoss(uint256 loss) public onlyPerpVault { require( - collateral.balanceOf(address(this)) == _totalCollateral - loss, + collateral.balanceOf(address(this)) >= _totalCollateral - loss, "Not enough collateral was sent out" ); _totalCollateral -= loss; }
Context
#0 - c4-pre-sort
2023-09-09T06:33:51Z
bytes032 marked the issue as duplicate of #619
#1 - c4-pre-sort
2023-09-09T06:33:56Z
bytes032 marked the issue as high quality report
#2 - c4-pre-sort
2023-09-11T16:14:08Z
bytes032 marked the issue as sufficient quality report
#3 - c4-judge
2023-10-20T20:01:34Z
GalloDaSballo marked the issue as satisfactory
#4 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#5 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#6 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#7 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#8 - c4-judge
2023-10-21T07:26:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#9 - c4-judge
2023-10-21T07:26:29Z
GalloDaSballo changed the severity to 3 (High Risk)
🌟 Selected for report: bart1e
Also found by: 0x3b, 0xCiphky, Aymen0909, HHK, Inspex, bin2chen, chaduke, gkrastenov, jasonxiale, josephdara, kodyvim, peakbolt, pep7siup, rokinot, rvierdiiev, tapir
181.367 USDC - $181.37
NFTs transferred to rdpxV2Core.sol
using UniV3LiquidityAMO.recoverERC721()
will be locked in rdpxV2Core.sol
Because rdpxV2Core.sol
doesn't have a way to handle NFTs and there is no way to transfer them out again
Currently UniV3LiquidityAMO.recoverERC721()
defaults to rdpxV2Core.sol
.
function recoverERC721( address tokenAddress, uint256 token_id ) external onlyRole(DEFAULT_ADMIN_ROLE) { // Only the owner address can ever receive the recovery withdrawal // INonfungiblePositionManager inherits IERC721 so the latter does not need to be imported INonfungiblePositionManager(tokenAddress).safeTransferFrom( address(this), @> rdpxV2Core, token_id ); emit RecoveredERC721(tokenAddress, token_id); }
But in the rdpxV2Core
contract, it can accept NFT
but there is no way to transfer out or handle NFTs.
The only interaction is with RdpxV2Bond
, but this is its own internal logic and will not transfer out, and transferring in will not work.
NFTs entering rdpxV2Core
will be locked in the contract forever.
Suggesting recoverERC721()
to transfer the NFT to msg.sender
would be a reasonable choice.
function recoverERC721( address tokenAddress, uint256 token_id ) external onlyRole(DEFAULT_ADMIN_ROLE) { // Only the owner address can ever receive the recovery withdrawal // INonfungiblePositionManager inherits IERC721 so the latter does not need to be imported INonfungiblePositionManager(tokenAddress).safeTransferFrom( address(this), - rdpxV2Core, + msg.sender, token_id ); emit RecoveredERC721(tokenAddress, token_id); }
ERC721
#0 - c4-pre-sort
2023-09-09T06:38:13Z
bytes032 marked the issue as duplicate of #106
#1 - c4-pre-sort
2023-09-12T06:09:55Z
bytes032 marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-09-12T06:12:26Z
bytes032 marked the issue as duplicate of #935
#3 - c4-judge
2023-10-20T18:05:28Z
GalloDaSballo changed the severity to 3 (High Risk)
#4 - c4-judge
2023-10-20T18:05:31Z
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
in PerpetualAtlanticVaultLP.deposit()
executing perpetualAtlanticVault.updateFunding()
after previewDeposit()
This results in previewDeposit()
not using the latest totalVaultCollateral
internally
The resulting shares
will be larger than the correct value
The current deposit()
implementation calculates shares
first with previewDeposit()
, and then executes the perpetualAtlanticVault.updateFunding()
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); _mint(receiver, shares); _totalCollateral += assets; emit Deposit(msg.sender, receiver, assets, shares); }
This results in previewDeposit()
not being calculated using the latest totalVaultCollateral
and the shares
obtained will be greater than the correct value!
function deposit( uint256 assets, address receiver ) public virtual returns (uint256 shares) { // Check for rounding error since we round down in previewDeposit. + perpetualAtlanticVault.updateFunding(); 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); _mint(receiver, shares); _totalCollateral += assets; emit Deposit(msg.sender, receiver, assets, shares); }
Context
#0 - c4-pre-sort
2023-09-07T13:39:47Z
bytes032 marked the issue as duplicate of #1948
#1 - c4-pre-sort
2023-09-07T13:42:30Z
bytes032 marked the issue as duplicate of #867
#2 - c4-pre-sort
2023-09-11T09:05:20Z
bytes032 marked the issue as sufficient quality report
#3 - c4-judge
2023-10-20T19:56:34Z
GalloDaSballo changed the severity to 3 (High Risk)
#4 - c4-judge
2023-10-20T19:56:48Z
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
miss reduce totalWethDelegated
in RdpxV2Core.withdraw()
The totalWethDelegated
that should have been reduced takes up available weth
, causing sync()
to throw an error.
Dependencies on the sync()
method will always revert
The current withdraw()
does not reduce totalWethDelegated
.
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; @> //miss reduce totalWethDelegated IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn); emit LogDelegateWithdraw(delegateId, amountWithdrawn); }
This results in totalWethDelegated
being much larger than it actually is!
The weth
available for the current contract is subtracted from the totalWethDelegated
.
This results in the available weth
being incorrect and much less.
A malicious user could repeatedly addToDelegate()
withdraw()
to make the number of available weth
0, causing all uses of sync()
to underflow
.
function sync() external { for (uint256 i = 1; i < reserveAsset.length; i++) { uint256 balance = IERC20WithBurn(reserveAsset[i].tokenAddress).balanceOf( address(this) ); if (weth == reserveAsset[i].tokenAddress) { @> balance = balance - totalWethDelegated; } reserveAsset[i].tokenBalance = balance; } emit LogSync(); }
The following code demonstrates, after addToDelegate/withdraw
,sync()
will underflow
add to Unit.t.sol
function testWithdrawSyncRevert() public{ console.log("rdpxV2Core weth balance(before)",weth.balanceOf(address(rdpxV2Core))); console.log("rdpxV2Core totalWethDelegated(before)",rdpxV2Core.totalWethDelegated()); uint256 delegateId = rdpxV2Core.addToDelegate(1e18, 10e8); rdpxV2Core.withdraw(delegateId); console.log("rdpxV2Core weth balance(after)",weth.balanceOf(address(rdpxV2Core))); console.log("rdpxV2Core totalWethDelegated(after)",rdpxV2Core.totalWethDelegated()); rdpxV2Core.sync(); }
$ forge test --match-test testWithdrawSyncReve Running 1 test for tests/rdpxV2-core/Unit.t.sol:Unit [FAIL. Reason: Arithmetic over/underflow] testWithdrawSyncRevert() (gas: 201496) Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 2.69s Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests) Failing tests: Encountered 1 failing test in tests/rdpxV2-core/Unit.t.sol:Unit [FAIL. Reason: Arithmetic over/underflow] testWithdrawSyncRevert() (gas: 201496)
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; + totalWethDelegated -=amountWithdrawn; IERC20WithBurn(weth).safeTransfer(msg.sender, amountWithdrawn); emit LogDelegateWithdraw(delegateId, amountWithdrawn); }
Context
#0 - c4-pre-sort
2023-09-07T07:37:07Z
bytes032 marked the issue as duplicate of #2186
#1 - c4-pre-sort
2023-09-07T07:37:18Z
bytes032 marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-09-07T07:37:39Z
bytes032 marked the issue as high quality report
#3 - bytes032
2023-09-07T07:38:01Z
Extensive POC
#4 - c4-judge
2023-10-20T17:52:48Z
GalloDaSballo marked the issue as selected for report
#5 - c4-judge
2023-10-21T07:31:59Z
GalloDaSballo marked issue #239 as primary and marked this issue as a duplicate of 239
#6 - c4-judge
2023-10-21T07:38:55Z
GalloDaSballo changed the severity to 3 (High Risk)
#7 - c4-judge
2023-10-21T07:41:40Z
GalloDaSballo marked the issue as partial-50
🌟 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
51.2629 USDC - $51.26
in reLP()
, Calculating mintokenAAmount
using the wrong formula can invalidate the slippage protection
ReLPContract.reLP()
The implementation is as follows:
function reLP(uint256 _amount) external onlyRole(RDPXV2CORE_ROLE) { ... @> mintokenAAmount = @> (((amountB / 2) * tokenAInfo.tokenAPrice) / 1e8) - @> (((amountB / 2) * tokenAInfo.tokenAPrice * slippageTolerance) / 1e16); uint256 tokenAAmountOut = IUniswapV2Router(addresses.ammRouter) .swapExactTokensForTokens( amountB / 2, mintokenAAmount, path, address(this), block.timestamp + 10 )[path.length - 1]; (, , uint256 lp) = IUniswapV2Router(addresses.ammRouter).addLiquidity( addresses.tokenA, addresses.tokenB, tokenAAmountOut, amountB / 2, 0, 0, address(this), block.timestamp + 10 ); // transfer the lp to the amo IERC20WithBurn(addresses.pair).safeTransfer(addresses.amo, lp); IUniV2LiquidityAmo(addresses.amo).sync(); // transfer rdpx to rdpxV2Core IERC20WithBurn(addresses.tokenA).safeTransfer( addresses.rdpxV2Core, IERC20WithBurn(addresses.tokenA).balanceOf(address(this)) ); IRdpxV2Core(addresses.rdpxV2Core).sync(); }
The formula for calculating mintokenAAmount
in the above code is
((amountB / 2) * tokenAInfo.tokenAPrice) / 1e8
amountB
is the amount of tokenB, but tokenAInfo.tokenAPrice
is the price of tokenA, which shouldn't be multiplied together
The correct one should be
((amountB / 2) * 1e8 / tokenAInfo.tokenAPrice)
function reLP(uint256 _amount) external onlyRole(RDPXV2CORE_ROLE) { ... - mintokenAAmount = - (((amountB / 2) * tokenAInfo.tokenAPrice) / 1e8) - - (((amountB / 2) * tokenAInfo.tokenAPrice * slippageTolerance) / 1e16); + mintokenAAmount = ((amountB / 2) * 1e8 / tokenAInfo.tokenAPrice) * (1e8 - slippageTolerance) / 1e8; uint256 tokenAAmountOut = IUniswapV2Router(addresses.ammRouter) .swapExactTokensForTokens( amountB / 2, mintokenAAmount, path, address(this), block.timestamp + 10 )[path.length - 1]; (, , uint256 lp) = IUniswapV2Router(addresses.ammRouter).addLiquidity( addresses.tokenA, addresses.tokenB, tokenAAmountOut, amountB / 2, 0, 0, address(this), block.timestamp + 10 ); // transfer the lp to the amo IERC20WithBurn(addresses.pair).safeTransfer(addresses.amo, lp); IUniV2LiquidityAmo(addresses.amo).sync(); // transfer rdpx to rdpxV2Core IERC20WithBurn(addresses.tokenA).safeTransfer( addresses.rdpxV2Core, IERC20WithBurn(addresses.tokenA).balanceOf(address(this)) ); IRdpxV2Core(addresses.rdpxV2Core).sync(); }
Context
#0 - c4-pre-sort
2023-09-09T05:11:47Z
bytes032 marked the issue as primary issue
#1 - c4-pre-sort
2023-09-11T06:58:04Z
bytes032 marked the issue as sufficient quality report
#2 - c4-sponsor
2023-09-25T18:54:45Z
psytama (sponsor) confirmed
#3 - c4-judge
2023-10-20T09:24:15Z
GalloDaSballo marked issue #1117 as primary and marked this issue as a duplicate of 1117
#4 - c4-judge
2023-10-20T19:55:13Z
GalloDaSballo marked the issue as selected for report
#5 - c4-judge
2023-10-20T19:55:35Z
GalloDaSballo marked issue #285 as primary and marked this issue as a duplicate of 285
#6 - c4-judge
2023-10-21T07:21:00Z
GalloDaSballo marked the issue as selected for report
🌟 Selected for report: QiuhaoLi
Also found by: 0xDING99YA, 0xvj, SBSecurity, T1MOH, Toshii, Udsen, bart1e, bin2chen, carrotsmuggler, pep7siup, said, sces60107, wintermute
90.6302 USDC - $90.63
RdpxV2Core._curveSwap
An error in the formula for calculating the minimum amount of minOut
for dpxEth
may cause the slippage protection to fail.
_curveSwap()
The code implementation is as follows:
function _curveSwap( uint256 _amount, bool _ethToDpxEth, bool validate, uint256 minAmount ) internal returns (uint256 amountOut) { IStableSwap dpxEthCurvePool = IStableSwap(addresses.dpxEthCurvePool); // First compute a reverse swapping of dpxETH to ETH to compute the amount of ETH required address coin0 = dpxEthCurvePool.coins(0); (uint256 a, uint256 b) = coin0 == weth ? (0, 1) : (1, 0); // validate the swap for peg functions if (validate) { uint256 ethBalance = IStableSwap(addresses.dpxEthCurvePool).balances(a); uint256 dpxEthBalance = IStableSwap(addresses.dpxEthCurvePool).balances( b ); _ethToDpxEth ? _validate( ethBalance + _amount <= (ethBalance + dpxEthBalance) / 2, 14 ) : _validate( dpxEthBalance + _amount <= (ethBalance + dpxEthBalance) / 2, 14 ); } // calculate minimum amount out uint256 minOut = _ethToDpxEth @> ? (((_amount * getDpxEthPrice()) / 1e8) - (((_amount * getDpxEthPrice()) * slippageTolerance) / 1e16)) : (((_amount * getEthPrice()) / 1e8) - (((_amount * getEthPrice()) * slippageTolerance) / 1e16)); // Swap the tokens amountOut = dpxEthCurvePool.exchange( _ethToDpxEth ? int128(int256(a)) : int128(int256(b)), _ethToDpxEth ? int128(int256(b)) : int128(int256(a)), _amount, minAmount > 0 ? minAmount : minOut ); }
From the code above we know that if _ethToDpxEth=true
uses the formula
minOut = (((_amount * getDpxEthPrice()) / 1e8) - (((_amount * getDpxEthPrice()) * slippageTolerance) / 1e16))
but getDpxEthPrice()
is the dpxEth
price.
The correct formula should be _amount * 1e8 / getDpxEthPrice()
function _curveSwap( uint256 _amount, bool _ethToDpxEth, bool validate, uint256 minAmount ) internal returns (uint256 amountOut) { .... // calculate minimum amount out - uint256 minOut = _ethToDpxEth - ? (((_amount * getDpxEthPrice()) / 1e8) - - (((_amount * getDpxEthPrice()) * slippageTolerance) / 1e16)) - : (((_amount * getEthPrice()) / 1e8) - - (((_amount * getEthPrice()) * slippageTolerance) / 1e16)); + uint256 minOut = _ethToDpxEth + ? (((_amount * 1e8) / getDpxEthPrice()) * (1e8 - slippageTolerance) / 1e8 + : (((_amount * 1e8) / getEthPrice()) * (1e8 - slippageTolerance) / 1e8; // Swap the tokens amountOut = dpxEthCurvePool.exchange( _ethToDpxEth ? int128(int256(a)) : int128(int256(b)), _ethToDpxEth ? int128(int256(b)) : int128(int256(a)), _amount, minAmount > 0 ? minAmount : minOut ); }
Context
#0 - c4-pre-sort
2023-09-10T09:54:47Z
bytes032 marked the issue as duplicate of #2172
#1 - c4-pre-sort
2023-09-12T04:26:22Z
bytes032 marked the issue as sufficient quality report
#2 - c4-pre-sort
2023-09-12T04:38:07Z
bytes032 marked the issue as duplicate of #970
#3 - c4-judge
2023-10-18T12:34:08Z
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
PerpetualAtlanticVault.updateFundingDuration()
Does not execute updateFunding()
before modifying fundingDuration
.
may cause the completed epoch
to fail to distribute rewards properly and immediately switch to the next epoch
.
Administrators can modify fundingDuration
function updateFundingDuration( uint256 _fundingDuration ) external onlyRole(DEFAULT_ADMIN_ROLE) { fundingDuration = _fundingDuration; }
The current implementation does not perform updateFunding()
first to distribute rewards that have already occurred
This may cause rewards that have already been completed but not yet counted to be deferred to the next cycle.
It is recommended to execute updateFunding()
before setting
function updateFundingDuration( uint256 _fundingDuration ) external onlyRole(DEFAULT_ADMIN_ROLE) { + updateFunding(); fundingDuration = _fundingDuration; } ## Assessed type Context
#0 - c4-pre-sort
2023-09-08T06:27:37Z
bytes032 marked the issue as duplicate of #980
#1 - c4-pre-sort
2023-09-11T08:21:53Z
bytes032 marked the issue as sufficient quality report
#2 - c4-judge
2023-10-20T11:12:10Z
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.addLiquidity()
-> UniV2LiquidityAMO._sendTokensToRdpxV2Core()
missing call rdpxV2Code.sync()
Causes reserveAsset[].tokenBalance
of rdpxV2Code
to be inaccurate
The bigger problem is that since sync()
is not executed, ADMIN
can skip checking the balance using the totalWethDelegated
balance
UniV2LiquidityAMO._sendTokensToRdpxV2Core()
lock of call rdpxV2Code.sync()
function _sendTokensToRdpxV2Core() internal { uint256 tokenABalance = IERC20WithBurn(addresses.tokenA).balanceOf( address(this) ); uint256 tokenBBalance = IERC20WithBurn(addresses.tokenB).balanceOf( address(this) ); // transfer token A and B from this contract to the rdpxV2Core IERC20WithBurn(addresses.tokenA).safeTransfer( addresses.rdpxV2Core, tokenABalance ); IERC20WithBurn(addresses.tokenB).safeTransfer( addresses.rdpxV2Core, tokenBBalance ); @> //miss call rdpxV2Code.sync() emit LogAssetsTransfered(msg.sender, tokenABalance, tokenBBalance); }
The result is that the tokenBalance
in rdpxV2Code
is briefly inaccurate and administrators can unrestricted use of the totalWethDelegated
The following test code demonstrates.
uniV2LiquidityAMO.addLiquidity()
illegally uses totalWethDelegated
causing the user to be unable to withdraw()
properly.
add to Periphery.t.sol
function testUseDelegateBalance() public { uniV2LiquidityAMO = new UniV2LiquidityAMO(); // set addresses uniV2LiquidityAMO.setAddresses( address(rdpx), address(weth), address(pair), address(rdpxV2Core), address(rdpxPriceOracle), address(factory), address(router) ); // give amo premission to access rdpxV2Core reserve tokens rdpxV2Core.addAMOAddress(address(uniV2LiquidityAMO)); rdpxV2Core.approveContractToSpend( address(rdpx), address(uniV2LiquidityAMO), type(uint256).max ); rdpxV2Core.approveContractToSpend( address(weth), address(uniV2LiquidityAMO), type(uint256).max ); rdpx.transfer(address(rdpxV2Core), 50e18); uint256 delegateId = rdpxV2Core.addToDelegate(10e18, 1e8); console.log("rdpxV2Core balance (before) :",weth.balanceOf(address(rdpxV2Core))); console.log("rdpxV2Core totalWethDelegated (before)",rdpxV2Core.totalWethDelegated()); // add liquidity uniV2LiquidityAMO.addLiquidity(50e18, 10e18, 0, 0); console.log("rdpxV2Core balance (after) :",weth.balanceOf(address(rdpxV2Core))); console.log("rdpxV2Core totalWethDelegated (after)",rdpxV2Core.totalWethDelegated()); vm.expectRevert(bytes("ERC20: transfer amount exceeds balance")); rdpxV2Core.withdraw(delegateId); }
$ forge test -vvv --match-test testUseDelegateBalance Running 1 test for tests/rdpxV2-core/Periphery.t.sol:Periphery [PASS] testUseDelegateBalance() (gas: 2294701) Logs: rdpxV2Core balance (before) : 10000000000000000000 rdpxV2Core totalWethDelegated (before) 10000000000000000000 rdpxV2Core balance (after) : 0 rdpxV2Core totalWethDelegated (after) 10000000000000000000 Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.99s Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
function _sendTokensToRdpxV2Core() internal { uint256 tokenABalance = IERC20WithBurn(addresses.tokenA).balanceOf( address(this) ); uint256 tokenBBalance = IERC20WithBurn(addresses.tokenB).balanceOf( address(this) ); // transfer token A and B from this contract to the rdpxV2Core IERC20WithBurn(addresses.tokenA).safeTransfer( addresses.rdpxV2Core, tokenABalance ); IERC20WithBurn(addresses.tokenB).safeTransfer( addresses.rdpxV2Core, tokenBBalance ); + addresses.rdpxV2Core.sync() emit LogAssetsTransfered(msg.sender, tokenABalance, tokenBBalance); }
Context
#0 - c4-pre-sort
2023-09-09T03:45:40Z
bytes032 marked the issue as duplicate of #798
#1 - c4-pre-sort
2023-09-09T04:09:36Z
bytes032 marked the issue as duplicate of #269
#2 - c4-pre-sort
2023-09-11T11:58:44Z
bytes032 marked the issue as sufficient quality report
#3 - c4-judge
2023-10-15T18:11:09Z
GalloDaSballo marked the issue as satisfactory