Platform: Code4rena
Start Date: 30/05/2023
Pot Size: $24,170 USDC
Total HM: 10
Participants: 5
Period: 3 days
Judge: hansfriese
Total Solo HM: 2
Id: 244
League: ETH
Rank: 3/5
Findings: 4
Award: $0.00
🌟 Selected for report: 3
🚀 Solo Findings: 0
🌟 Selected for report: rbserver
Also found by: adriro, bin2chen, d3e4, minhquanym
Data not available
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L216 https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L744
The borrower can potentially block the liquidation and auction processed by using a contract and reverting on ETH transfers.
When a loan is being liquidated or auctioned, any credit still available to the borrower (i.e. margin discounting the lender's interests) needs to be refunded. This is present in the withdrawEthWithInterest()
function, that can be used to liquidate a position, and in the auctionBuyNft()
function which is used to auction a loan.
192: function withdrawEthWithInterest(Lien calldata lien, uint256 lienId) external override validateLien(lien, lienId) { ... 214: // transfer PnL to borrower 215: if (lien.credit > payableInterest) { 216: payable(lien.borrower).transfer(lien.credit - payableInterest); 217: } ...
688: function auctionBuyNft( 689: Lien calldata lien, 690: uint256 lienId, 691: uint256 tokenId, 692: uint256 amount 693: ) external override validateLien(lien, lienId) auctionLive(lien) { ... 741: // pay PnL to borrower 742: uint256 payback = lien.credit + lien.price - payableInterest - amount; 743: if (payback > 0) { 744: payable(lien.borrower).transfer(payback); 745: } ...
As we can see in both of these cases, if the borrower still has some credit left, this is refunded by using the transfer()
function. This is problematic, as the transfer()
function reverts on failure. If the receiver is a contract, then this failure can be easily manipulated and forced.
This means that the borrower can use a contract to take the loan and arbitrarily decide to revert when the payback is being refunded, effectively blocking the liquidation and auction processes.
The attacker can create a simple contract to interact with the Particle exchange. This contract has a receive()
function that can be configured to revert.
function receive() external payable { if (_shouldRevert) { revert(); } }
The attacker then takes loans using this contract as an interface, and can block at will auctions or liquidations by setting _shouldRevert
to true.
Similar to how interests are accrued for the lender, prefer a pull over push process to have the receiver pull ETH instead of pushing it in the middle of a transaction. The withdrawEthWithInterest()
and auctionBuyNft()
functions should accumulate amounts corresponding to the borrower in internal storage and then offer a function to withdraw these by separate.
DoS
#0 - c4-judge
2023-06-06T06:33:23Z
hansfriese marked the issue as satisfactory
#1 - c4-judge
2023-06-06T06:34:06Z
hansfriese changed the severity to 3 (High Risk)
#2 - c4-judge
2023-06-06T06:34:21Z
hansfriese marked the issue as primary issue
#3 - c4-judge
2023-06-06T07:33:58Z
hansfriese marked the issue as duplicate of #31
#4 - wukong-particle
2023-06-07T01:38:22Z
Judge is correct, indeed duplication
#5 - wukong-particle
2023-06-07T01:40:00Z
will update our contract following the pull based approach to aggregate gains to trader account level
#6 - c4-sponsor
2023-06-07T01:40:06Z
wukong-particle marked the issue as sponsor confirmed
#7 - wukong-particle
2023-06-12T23:05:32Z
will update our contract following the pull based approach to aggregate gains to trader account level
^ fixed with https://github.com/Particle-Platforms/particle-exchange-protocol/pull/13
🌟 Selected for report: adriro
Also found by: minhquanym, rbserver
Data not available
withdrawEthWithInterest()
The withdrawEthWithInterest()
function fails to collect treasury fees from the lender interests.
The Particle exchange collects treasury fees from the lender's interests. These interests are accumulated in the interestAccrued
mapping and are withdrawn using the _withdrawAccountInterest()
function, which splits the portion that corresponds to the treasury.
231: function _withdrawAccountInterest(address payable lender) internal { 232: uint256 interest = interestAccrued[lender]; 233: if (interest == 0) return; 234: 235: interestAccrued[lender] = 0; 236: 237: if (_treasuryRate > 0) { 238: uint256 treasuryInterest = MathUtils.calculateTreasuryProportion(interest, _treasuryRate); 239: _treasury += treasuryInterest; 240: interest -= treasuryInterest; 241: } 242: 243: lender.transfer(interest); 244: 245: emit WithdrawAccountInterest(lender, interest); 246: }
Lines 238-240 calculate treasury fees and accumulate them in the _treasury
variable, which are later withdrawn by the owner using the withdrawTreasury()
function.
However, these fees fail to be considered in the case of withdrawEthWithInterest()
:
192: function withdrawEthWithInterest(Lien calldata lien, uint256 lienId) external override validateLien(lien, lienId) { 193: if (msg.sender != lien.lender) { 194: revert Errors.Unauthorized(); 195: } 196: 197: if (lien.loanStartTime == 0) { 198: revert Errors.InactiveLoan(); 199: } 200: 201: uint256 payableInterest = _calculateCurrentPayableInterest(lien); 202: 203: // verify that the liquidation condition has met (borrower insolvent or auction concluded) 204: if (payableInterest < lien.credit && !_auctionConcluded(lien.auctionStartTime)) { 205: revert Errors.LiquidationHasNotReached(); 206: } 207: 208: // delete lien (delete first to prevent reentrancy) 209: delete liens[lienId]; 210: 211: // transfer ETH with interest back to lender 212: payable(lien.lender).transfer(lien.price + payableInterest); 213: 214: // transfer PnL to borrower 215: if (lien.credit > payableInterest) { 216: payable(lien.borrower).transfer(lien.credit - payableInterest); 217: } 218: 219: emit WithdrawETH(lienId); 220: 221: // withdraw interest from this account too 222: _withdrawAccountInterest(payable(msg.sender)); 223: }
As we can see in the previous snippet of code, the interests are calculated in line 201 but that amount is then transferred, along with the lien price, back to the lender in full in line 212, without deducting any treasury fees.
The interest can be simply accumulated in the interestAccrued
mapping, which are later withdrawn (correctly taking into account treasury fees) in the already present call to _withdrawAccountInterest()
.
function withdrawEthWithInterest(Lien calldata lien, uint256 lienId) external override validateLien(lien, lienId) { if (msg.sender != lien.lender) { revert Errors.Unauthorized(); } if (lien.loanStartTime == 0) { revert Errors.InactiveLoan(); } uint256 payableInterest = _calculateCurrentPayableInterest(lien); // verify that the liquidation condition has met (borrower insolvent or auction concluded) if (payableInterest < lien.credit && !_auctionConcluded(lien.auctionStartTime)) { revert Errors.LiquidationHasNotReached(); } // delete lien (delete first to prevent reentrancy) delete liens[lienId]; + // accrue interest to lender + interestAccrued[lien.lender] += payableInterest; @ // transfer ETH back to lender @ payable(lien.lender).transfer(lien.price); // transfer PnL to borrower if (lien.credit > payableInterest) { payable(lien.borrower).transfer(lien.credit - payableInterest); } emit WithdrawETH(lienId); // withdraw interest from this account too _withdrawAccountInterest(payable(msg.sender)); }
Other
#0 - c4-judge
2023-06-06T06:21:39Z
hansfriese marked the issue as primary issue
#1 - c4-judge
2023-06-06T06:22:26Z
hansfriese marked the issue as satisfactory
#2 - c4-sponsor
2023-06-06T19:50:25Z
wukong-particle marked the issue as sponsor acknowledged
#3 - wukong-particle
2023-06-06T19:52:04Z
We will likely fix the issue in another way. We will modify withdrawNftWithInterest
and withdrawEthWithInterest
into withdrawNft
and withdrawEth
, i.e., move the interest withdraw into the single account level interest withdraw function (similar to the suggestion made in https://github.com/code-423n4/2023-05-particle-findings/issues/31)
#4 - c4-judge
2023-06-07T13:54:39Z
hansfriese marked the issue as selected for report
#5 - hansfriese
2023-06-07T17:22:04Z
After discussion, I think that HIGH is the appropriate severity because this issue incurs loss for the protocol.
#6 - c4-judge
2023-06-07T17:22:14Z
hansfriese changed the severity to 3 (High Risk)
#7 - wukong-particle
2023-06-08T06:05:25Z
Data not available
Judge has assessed an item in Issue #28 as 2 risk. The relevant finding follows:
[L-9] Griefer can DoS lender NFT withdrawals
#0 - c4-judge
2023-06-08T08:16:51Z
hansfriese marked the issue as duplicate of #44
#1 - c4-judge
2023-06-08T08:17:53Z
hansfriese marked the issue as satisfactory
🌟 Selected for report: minhquanym
Also found by: adriro
Data not available
buyNftFromMarket()
In the buyNftFromMarket()
function, the borrower buys an NFT in order to repay and close their loan. The purchase is executed in the internal function named _execBuyNftFromMarket()
.
395: function _execBuyNftFromMarket( 396: address collection, 397: uint256 tokenId, 398: uint256 amount, 399: uint256 useToken, 400: address marketplace, 401: bytes calldata tradeData 402: ) internal { 403: if (!registeredMarketplaces[marketplace]) { 404: revert Errors.UnregisteredMarketplace(); 405: } 406: 407: uint256 balanceBefore = address(this).balance; 408: 409: // execute raw order on registered marketplace 410: bool success; 411: if (useToken == 0) { 412: // use ETH 413: // solhint-disable-next-line avoid-low-level-calls 414: (success, ) = marketplace.call{value: amount}(tradeData); 415: } else if (useToken == 1) { 416: // use WETH 417: weth.deposit{value: amount}(); 418: weth.approve(marketplace, amount); 419: // solhint-disable-next-line avoid-low-level-calls 420: (success, ) = marketplace.call(tradeData); 421: } 422: 423: if (!success) { 424: revert Errors.MartketplaceFailedToTrade(); 425: } 426: 427: // verify that the declared NFT is acquired and the balance decrease is correct 428: if (IERC721(collection).ownerOf(tokenId) != address(this) || balanceBefore - address(this).balance != amount) { 429: revert Errors.InvalidNFTBuy(); 430: } 431: }
As we can see in the previous snippet of code, there are mainly two paths: use ETH or use WETH, which mainly depends on the specifics of the selected marketplace.
The ETH path is straightforward, the specified amount is sent as callvalue to the marketplace. If the difference doesn't match after the execution, because some ETH was refunded or something went wrong, it would cause a revert (line 428).
However, the WETH side is a bit different. Line 417 wraps the specified amount as WETH which is then approved to the marketplace. Now, if the marketplace doesn't consume the full amount, then any unused amount will still be present in the contract as WETH, causing the check in line 428 to succeed. In fact, if the intention is to use WETH (useToken == 1
), the check in line 428 will always succeed as the amount
parameter is transferred to the WETH contract causing balanceBefore - address(this).balance == amount
to always be true.
Let's say Bob wants to purchase an NFT to close his loan and submits an amount of 5 ether. The selected marketplace uses WETH so 5 ether are wrapped as WETH. The NFT is listed as 4.5 ETH and has an associated fee of 10%. The marketplace then pulls 4.95 WETH from the exchange contract. This leaves 0.05 WETH in the contract that is deducted from Bob's payback, while the check succeeds as the difference in ETH is still 5 ether.
The implementation can unwrap any available WETH after the execution of the marketplace operation so that it can be considered in the calculation present in the amount
validation.
function _execBuyNftFromMarket( address collection, uint256 tokenId, uint256 amount, uint256 useToken, address marketplace, bytes calldata tradeData ) internal { if (!registeredMarketplaces[marketplace]) { revert Errors.UnregisteredMarketplace(); } uint256 balanceBefore = address(this).balance; // execute raw order on registered marketplace bool success; if (useToken == 0) { // use ETH // solhint-disable-next-line avoid-low-level-calls (success, ) = marketplace.call{value: amount}(tradeData); } else if (useToken == 1) { // use WETH weth.deposit{value: amount}(); weth.approve(marketplace, amount); // solhint-disable-next-line avoid-low-level-calls (success, ) = marketplace.call(tradeData); } if (!success) { revert Errors.MartketplaceFailedToTrade(); } + uint256 wethBalance = weth.balanceOf(address(this)); + if (wethBalance > 0) { + weth.withdraw(wethBalance); + } // verify that the declared NFT is acquired and the balance decrease is correct if (IERC721(collection).ownerOf(tokenId) != address(this) || balanceBefore - address(this).balance != amount) { revert Errors.InvalidNFTBuy(); } }
ERC20
#0 - c4-judge
2023-06-06T06:56:21Z
hansfriese marked the issue as satisfactory
#1 - c4-judge
2023-06-06T07:00:38Z
hansfriese marked the issue as duplicate of #7
#2 - wukong-particle
2023-06-07T01:35:09Z
Agreed with the judge, indeed duplication.
#3 - c4-sponsor
2023-06-07T01:35:16Z
wukong-particle marked the issue as sponsor confirmed
#4 - wukong-particle
2023-06-12T23:01:50Z
🌟 Selected for report: adriro
Also found by: bin2chen, d3e4, minhquanym, rbserver
Data not available
Issue | Instances | |
---|---|---|
NC-1 | Use named parameters for mapping type declarations | 3 |
NC-2 | Unneeded explicit return | 1 |
NC-3 | Use bool type for togglable parameter | 1 |
Consider using named parameters in mappings (e.g. mapping(address account => uint256 balance)
) to improve readability. This feature is present since Solidity 0.8.18
Instances (3):
File: contracts/protocol/ParticleExchange.sol 24: mapping(uint256 => bytes32) public liens; 25: mapping(address => uint256) public interestAccrued; 26: mapping(address => bool) public registeredMarketplaces;
The explicit return can be omitted as the function is using named return variables.
Instances (1):
Instances (1):
Issue | |
---|---|
L-1 | Contract files should define a locked compiler version |
L-2 | _withdrawAccountInterest() can be front-runned to increase the treasury rate |
L-3 | Relax amount check strictness while operating with marketplaces |
L-4 | The onERC721Received callback can be used to create fake liens |
L-5 | Accidental loss of NFTs due to misuse of push mechanism |
L-6 | auctionBuyNft() should use current auction price instead of amount parameter |
L-7 | Use Ownable2Step instead of Ownable for access control |
L-8 | Provide safer limits for treasury rate |
L-9 | Griefer can DoS lender NFT withdrawals |
L-10 | Marketplace calls are too permissive |
Contracts should be deployed with the same compiler version and flags that they have been tested with thoroughly. Locking the pragma helps to ensure that contracts do not accidentally get deployed using, for example, an outdated compiler version that might introduce bugs that affect the contract system negatively.
Instances (1):
File: contracts/protocol/ParticleExchange.sol 2: pragma solidity ^0.8.17;
_withdrawAccountInterest()
can be front-runned to increase the treasury rateA call to _withdrawAccountInterest()
can be front-runned to increase the treasury rate and diminish (or nullify) the lender's portion of the loan interests.
function _withdrawAccountInterest(address payable lender) internal { uint256 interest = interestAccrued[lender]; if (interest == 0) return; interestAccrued[lender] = 0; if (_treasuryRate > 0) { uint256 treasuryInterest = MathUtils.calculateTreasuryProportion(interest, _treasuryRate); _treasury += treasuryInterest; interest -= treasuryInterest; } lender.transfer(interest); emit WithdrawAccountInterest(lender, interest); }
The treasuryInterest
variable is calculated as a proportion of the interest
amount which is then subtracted to calculate the lender's share.
In both sellNftToMarket()
and buyNftFromMarket()
the given amount is validated using a strict equality check.
These conditions can be relaxed to account for extra fees, rounding or potential minimal differences when the transaction gets executed. For example, the sell operation can check if the difference in balance is greater or equal than the amount (i.e. it received at least amount
) and the buy operation can check the the balance differences is lower or equal than the amount (i.e. it sent at most amount
).
onERC721Received
callback can be used to create fake lienshttps://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L74
Since the onERC721Received
callback is a public function it can be called by anyone to create fake liens. The from
parameter is user supplied to the function, which means that anyone can create fake liens on behalf of an arbitrary lender.
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L74
An accidental loss of NFTs can happen if the sender doesn't use safeTransferFrom()
or submits an incorrect payload in safeTransferFrom()
.
auctionBuyNft()
should use current auction price instead of amount
parameterThe amount
parameter in the auctionBuyNft()
function should be used as a slippage check to ensure the caller gets at least an amount
value from the action, but the effective value the offerer receives should be currentAuctionPrice
(as this represents the maximum incentive the offerer gets while executing the action).
Use the Ownable2Step variant of the Ownable contract to better safeguard against accidental transfers of access control.
The current implementation of setTreasuryRate()
only limits the rate parameter to _BASIS_POINTS
, which if maxed represents the 100% of the lender's earnings.
A malicious actor can front-runs calls to withdrawNftWithInterest()
to prevent the lender from claiming the NFT and deleting the lien, by taking a loan of the lien and withdrawing the NFT, causing the transaction to withdrawNftWithInterest()
to fail. The malicious borrower can then immediately repay the loan with zero or minimum interests.
The sellNftToMarket()
and buyNftFromMarket()
functions present in the ParticleExchange contract are used to execute a sell or a buy operation in a registered marketplace.
Even though marketplaces are whitelisted, both of these functions take an arbitrary tradeData
payload that is supplied by the caller. This argument is then used as the calldata to the marketplace functions.
An attacker could use this to essentially call any function in the registered marketplace.
The recommendation here is to also whitelist function selectors and validate the right calls are being made. For example, this could be implemented as a mapping(address => mapping(bytes4 => bool))
that indicates whether a particular function signature is enabled for the corresponding marketplace.
#0 - c4-judge
2023-06-06T10:47:20Z
hansfriese marked the issue as grade-a
#1 - c4-sponsor
2023-06-06T19:58:40Z
wukong-particle marked the issue as sponsor acknowledged
#2 - wukong-particle
2023-06-06T20:00:13Z
NC-1, Current compiler preference is ^0.8.17, so 0.8.17 hasn't yet supported this
NC-2, Acknowledged, explicit return for readability
NC-3, Used uint256
instead of bool
for better gas
#3 - wukong-particle
2023-06-06T20:25:31Z
L-1, will likely use 0.8.19, this duplicates the slither findings in https://github.com/code-423n4/2023-05-particle/tree/main/slither#pragma
L-2, IIUC, same as https://github.com/code-423n4/2023-05-particle-findings/issues/9
L-3, we impose strict wei-level check to ensure no leaky bucket. Extra fees should be included in the "amount" argument.
L-4, understood, but this is a designed case where anyone can supply NFT using this push-based method via onERC721Received
, the "arbitrary" lender specified by "from" is as if this "from" address calls "supplyNft" directly, should be a benign behavior
L-5, understood, this push-based method should only be used by our front-end, we are not liable for the accidental loss via direct interaction with the contract
L-6, acknowledged, will use this suggestion
L-7, acknowledged, will consider using Ownable2Step
L-8, acknowledged, will provide a safer check
L-9, understood, this should be allowed because this is a pricy attack as circulating nft in real market has a bid-ask spread that the attacker needs to fill
L-10, understood, will consider the function check, though the check before/after the arbitrary function call should strictly prevent all mis-use cases
#4 - hansfriese
2023-06-07T09:36:09Z
All findings are valid.
L10 N3
#5 - hansfriese
2023-06-07T09:43:48Z
#22, #23 were downgraded to LOW,
L12 N3
Marking the best
#6 - c4-judge
2023-06-07T09:43:54Z
hansfriese marked the issue as selected for report
#7 - wukong-particle
2023-06-07T20:44:11Z
#8 - wukong-particle
2023-06-09T22:12:26Z
L2 mitigated with https://github.com/Particle-Platforms/particle-exchange-protocol/pull/12 L6 fixed with https://github.com/Particle-Platforms/particle-exchange-protocol/pull/8 L8 fixed with https://github.com/Particle-Platforms/particle-exchange-protocol/pull/12 L9 similar to https://github.com/code-423n4/2023-05-particle-findings/issues/44#issuecomment-1585012812
NC1 fixed with https://github.com/Particle-Platforms/particle-exchange-protocol/commit/66b29d522600f54325e307cca77975def925f920 NC2 fixed with https://github.com/Particle-Platforms/particle-exchange-protocol/pull/18 NC3 not binary toggle, in the future we may allow other WETH equivalent tokens
🌟 Selected for report: adriro
Also found by: d3e4, minhquanym
Data not available
Function calculateCurrentInterest()
uses unnecessary intermediary WAD scaling resulting in unneeded multiplications and divisions as a result of wadMul
and wadDiv
.
The calculation can be simplified as (principal * rateBps * (block.timestamp - loanStartTime)) / (_BASIS_POINTS * 365 days)
.
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/libraries/math/MathUtils.sol#L19
Function calculateCurrentAuctionPrice()
uses unnecessary intermediary WAD scaling resulting in unneeded multiplications and divisions as a result of wadMul
and wadDiv
.
The calculation can be simplified as (price * auctionElapsed / auctionDuration
.
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/libraries/math/MathUtils.sol#L36
Function calculateTreasuryProportion()
uses unnecessary intermediary WAD scaling resulting in unneeded multiplications and divisions as a result of wadMul
and wadDiv
.
The calculation can be simplified as (interest * rateBips / _BASIS_POINTS
.
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/libraries/math/MathUtils.sol#L50
Storage variables _treasuryRate
and _treasury
could benefit from being packed together in a single slot as these are used in conjunction. This could be achieved by changing _treasuryRate
to be a uint24
(which can safely fit the maximum rate of 100_000) and reducing the size of _treasury
to uint232 (which can still represent a huge amount of ETH).
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L22-L23
In withdrawEthWithInterest()
there are potentially multiple calls to the lender to send ETH. Group these together in order to execute a single transfer()
call.
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L212
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L222
In the _withdrawAccountInterest()
function the _treasuryRate
variable is read twice from storage. Consider caching it locally to execute a single SLOAD.
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L237
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L238
In the _withdrawAccountInterest()
function, the math in _treasury += treasuryInterest
can be unchecked as it would be practically impossible to overflow the size of the _treasury
variable.
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L239
In the _withdrawAccountInterest()
function, the math in interest -= treasuryInterest
can be unchecked as treasuryInterest
is a portion of the interest
(i.e. treasuryInterest <= interest
).
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L240
If marketplaces are trusted entities then WETH approval can be done once for an infinite amount instead of approving a specific amount in each call to _execBuyNftFromMarket()
.
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L418
ERC721 transfers can be safely executed by calling transferFrom()
instead of safeTransferFrom()
when the recipient is the exchange contract in order to avoid the unneeded callback and save gas.
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L57
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L499
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L731
In the stopLoanAuction()
function the check lien.loanStartTime == 0
is unneeded this is already implied by the check below of lien.auctionStartTime == 0
(i.e. if lien.auctionStartTime != 0
is true then it already implies that the lien is taken).
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L659
In the auctionBuyNft()
function, the math in lien.credit + lien.price - payableInterest - amount
can be unchecked as amount
is lower than or equal to currentAuctionPrice
, and currentAuctionPrice
is a portion of maxSpendable
(lien.credit + lien.price - payableInterest
), which means that lien.credit + lien.price - payableInterest
is safe (as it didn't overflow in line 699) and lien.credit + lien.price - payableInterest - amount
is also safe as amount <= lien.credit + lien.price - payableInterest
.
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L742
In the withdrawTreasury()
function, the _treasury
storage variable is read twice from storage. Consider caching it locally to execute a single SLOAD.
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L809
https://github.com/code-423n4/2023-05-particle/blob/main/contracts/protocol/ParticleExchange.sol#L813
#0 - c4-judge
2023-06-06T10:45:16Z
hansfriese marked the issue as grade-a
#1 - c4-sponsor
2023-06-06T20:31:30Z
wukong-particle marked the issue as sponsor acknowledged
#2 - wukong-particle
2023-06-06T20:32:44Z
If marketplaces are trusted entities then WETH approval can be done once for an infinite amount instead of approving a specific amount in each call to
_execBuyNftFromMarket()
.
Understood, we only transfer declared amount to WETH to avoid potential attack.
Agreed with all other gas optimization suggestions, will consider incorporating all of them.
#3 - c4-judge
2023-06-07T12:43:06Z
hansfriese marked the issue as selected for report
#4 - wukong-particle
2023-06-08T17:04:03Z
MathUtils library fixed in https://github.com/Particle-Platforms/particle-exchange-protocol/pull/11
#5 - wukong-particle
2023-06-09T23:09:06Z
Unchecked treasury rate fixed in https://github.com/Particle-Platforms/particle-exchange-protocol/pull/19 safeTransferFrom fixed in https://github.com/Particle-Platforms/particle-exchange-protocol/pull/21 stopAuction check fixed in https://github.com/Particle-Platforms/particle-exchange-protocol/pull/22 SLOAD _treasury once fixed in https://github.com/Particle-Platforms/particle-exchange-protocol/pull/23 withdrawEthWithInterest transfer fixed (nullified) with https://github.com/Particle-Platforms/particle-exchange-protocol/pull/10