Guardian's review of Buttonwood Cash for Buttonwood, published October 2025. The report records 100 findings across 3 review rounds, including 3 critical and 9 high.
- Published
- Review window
- July 21 to September 28, 2025
- Rounds
- Main Review, Remediation Review, Remediation Review 2
- Language
- Solidity
- Chains
- Hyperliquid
- Sector
- Lending
- 3 Critical
- 9 High
- 20 Medium
- 66 Low
- 2 Informational
Scope
36 files in scope · 2,524 nSLOC
| File | nSLOC | Lines |
|---|---|---|
src/UsdxQueue.sol | 36 | 75 |
src/USDX.sol | 56 | 115 |
src/SubConsol.sol | 65 | 153 |
src/RebasingERC20.sol | 56 | 107 |
src/PythPriceOracle.sol | 43 | 108 |
src/PythInterestRateOracle.sol | 48 | 131 |
src/OriginationPoolScheduler.sol | 228 | 489 |
src/OriginationPool.sol | 121 | 231 |
src/OrderPool.sol | 108 | 203 |
src/MultiTokenVault.sol | 121 | 253 |
src/MortgageQueue.sol | 111 | 216 |
src/MortgageNFT.sol | 60 | 133 |
src/LoanManager.sol | 216 | 434 |
src/LenderQueue.sol | 81 | 172 |
src/GeneralManager.sol | 383 | 745 |
src/ForfeitedAssetsQueue.sol | 37 | 77 |
src/ForfeitedAssetsPool.sol | 74 | 157 |
src/ConversionQueue.sol | 155 | 323 |
src/Consol.sol | 62 | 124 |
src/libraries/SharesMath.sol | 28 | 61 |
src/libraries/Roles.sol | 4 | 11 |
src/libraries/MortgageMath.sol | 276 | 543 |
src/libraries/Constants.sol | 12 | 55 |
src/types/WithdrawalRequest.sol | 8 | 20 |
src/types/TokenScalars.sol | 5 | 12 |
src/types/OriginationPoolConfig.sol | 14 | 30 |
src/types/OPoolConfigId.sol | 8 | 22 |
src/types/MortgagePosition.sol | 25 | 52 |
src/types/MortgageNode.sol | 8 | 20 |
src/types/enums/OriginationPoolPhase.sol | 6 | 14 |
src/types/enums/MortgageStatus.sol | 6 | 14 |
src/types/orders/PurchaseOrder.sol | 13 | 29 |
src/types/orders/OriginationParameters.sol | 11 | 24 |
src/types/orders/OrderRequests.sol | 20 | 46 |
src/types/orders/OrderAmounts.sol | 6 | 14 |
src/types/orders/MortgageParams.sol | 13 | 28 |
Findings 100
Main Review
64 findings · July 21 to August 4, 2025-
C-01 Critical Expired Orders Do Not Refund Tokens Logical Error Resolved
Description
When a user requests a mortgage, either the collateral token or USDX is transferred from the user to the order pool, depending on if the request is compounding or not.
If the order expires before it is processed, the order is simply cancelled and the user's tokens are not returned. The order is deleted.
function _processOrder(uint256 index, uint256 hintPrevId) internal returns (uint256 collectedGasFee) { // Fetch the order from the orders mapping PurchaseOrder memory order = _orders[index]; // If the order has expired, cancel it. Otherwise, process it. if (order.expiration < block.timestamp) { // Cancel the mortgage request IGeneralManager(generalManager).burnMortgageNFT(order.mortgageParams.tokenId); // Delete the order delete _orders[index]; // Emit the PurchaseOrderExpired event emit PurchaseOrderExpired(index);Recommendation
Return the user's collateral or USDX depending on the type of order.
-
C-02 Critical Missing Access Control In Callback Access Control Resolved
Description
The
originationPoolDeployCallbackfunction lacks access control. A malicious actor can invoke it directly with acollateralAmountof 0 and an excessively largeamountBorrowed. This results in the minting ofamountBorrowedConsol tokens to the malicious user.Recommendation
Implement access control for the
originationPoolDeployCallbackfunction to prevent unauthorized usage. -
C-03 Critical All USDX Withdrawals Are DoS'ed DoS Resolved
Description
Cancelled withdrawal requests are not removed from the queue; instead, the
request.amountandrequest.sharevalues are set to 0 to allow these requests to be skipped.However, the
UsdxQueue.processWithdrawalRequestsfunction does not skip these cancelled requests and attempts to withdraw zero amounts from the Consol, which reverts with anAmountTooSmallerror.Since withdrawal processing is linear, subsequent requests cannot be processed until the previous ones are completed. As a result, a single cancellation causes all subsequent withdrawal requests to be effectively DoSed.
Note that the exact same issue occurs in
ForfeitedAssetsQueue.processWithdrawalRequestsfunction as well.Recommendation
Skip cancelled requests. Do not withdraw from Consol if
request.amountis zero. -
H-01 High Compounding Orders Cannot Be Processed Logical Error Resolved
Description
Users create mortgage requests in the
GeneralManagercontract, and these requests are processed by the fulfiller using theprocessOrdersfunction in theOrderPool.During this process, the
GeneralManager.originatefunction is called, which subsequently callsOriginationPool.deploy, and finally invokes theoriginationPoolDeployCallbackfunction inGeneralManager. The_enqueueMortgagefunction is called within this callback whenoriginationParameters.conversionQueueis non-zero.However, the
_enqueueMortgagefunction requires themortgageGasFeeto be sent asmsg.value, but neither theoriginatenor theoriginationPoolDeployCallbackfunctions are payable, making it impossible to send this fee asmsg.value. As a result,processOrderswill always fail when a mortgage request has aconversionQueuewith a non-zeromortgageGasFee.Since compounding orders must include a conversionQueue, this issue will have the greatest impact on them. However, it is not limited to compounding orders, as non-compounding orders can also have a
conversionQueueset.Recommendation
Ensure that the required
mortgageGasFeeis passed asmsg.valueduring theprocessOrdersflow. However, since the fulfiller will invoke this function, the required amount should be transferred from the user to the fulfiller at the time of order creation.Another important consideration is that the fulfiller can process multiple orders at once and directly using
msg.valuewithin theprocessOrdersloop may introduce new issues. Care should be taken when sending value, and only the required amount for each order should be sent. -
H-02 High Mortgage Queue DoS Due To Unused Hint DoS Resolved
Description
In the
_insertMortgagefunction of the MortgageQueue contract thehintPrevIdvalue is only validated and does not functionally serve to reduce the iterations necessary to lodge a mortgage in the right spot in the queue.When the
triggerPriceof the head is less than the triggerPrice of the newly inserted mortgage thehintPrevIdalways begins from the head. Therefore the insertion must always traverse the full set of nodes to insert a new mortgage.In many cases the queue will either see an amount of natural usage or an intentional malicious amount of usage such that subsequent mortgages require more iterations than the block gas limit to insert. This would prevent the origination process for compounding mortgages from being fulfilled as well as other processes in the Buttonwood protocol that rely on this underlying queue logic.
Recommendation
Do not reset the
hintPrevIdto the head node if thehintPrevIdhas already proven to have a trigger price less than or equal to thetriggerPriceof the new mortgage. -
M-01 Medium MultiTokenVault Inflation Attack Gaming Acknowledged
Description
In the
MultiTokenVaultthe inflation attack is defended against by using adecimalsOffsetto preserve a larger amount of shares relative to the number of assets deposited.However this mechanism can be circumvented because there is a public
forfeitfunction which can be used to burn shares of the vault and counteract the decimalsOffset, allowing the total initial shares to be manipulated back down to 1 wei by an attacker.As a result the typical inflation attack can be carried out, resulting in the victim receiving 0 or significantly reduced shares of the vault.
Recommendation
Firstly, make the
forfeitfunction only callable by the LoanManager contract.Even then the inflation attack may still be possible by transferring Consol tokens directly to the
LoanManagerbefore it invokes forfeit with it’s total balance. Consider either requiring that some initial shares are minted to the dead address, or that forfeit cannot be used to forfeit shares below a certain reasonable threshold of totalShares. -
M-02 Medium enqueueMortgage Cannot Accept Gas Fees Unexpected Behavior Resolved
Description
In the
GeneralManagercontract theenqueueMortgagefunction is not marked aspayableand therefore cannot accept the necessary Ether to call theconversionQueuewith the expected value for gas fees.As a result the
enqueueMortgagefunction cannot be used to enqueue a mortgage as expected.Recommendation
Make the external
enqueueMortgagefunction on theGeneralManagerpayable so that it can accept the necessary gas fee ether to send to theconversionQueue. -
M-03 Medium Incorrect Async Usage Logical Error Resolved
Description
In the
LoanManagerredeemMortgagefunction if the async value is provided as true the non-async path will be taken and if theasyncvalue is false the asynchronous path will be taken.Recommendation
Flip the case such that the
asyncpath is taken if the async value is true. -
M-04 Medium ExpandBalanceSheet Allows Lower Rate Gaming Resolved
Description
The
expandBalanceSheetaction does not perform any validation to ensure that the expansion totalPeriods matches the totalPeriods of the existing mortgage.As a result, users may create a minimum size expansion directly after initiating a 3-year mortgage at a lower rate, as the three-year treasury rate will typically be lower than the fiver year treasury rate, and expand their existing mortgage to be extended to 5 years while maintaining this lower rate.
This way those who are given the expansion role are able to game the protocol and obtain a 5-year length mortgage at the rate of a 3-year length mortgage. Even without paying any kind of refinance fee.
Recommendation
Validate that an expansion can only be made with a totalPeriods value that matches the existing mortgage.
-
M-05 Medium Expand Balance Sheet Has No Restrictions Validation Resolved
Description
The expandBalanceSheet function in the GeneralManager performs no validations that the caller is the owner of the balance sheet being expanded, this allows approved callers of the expandBalanceSheet function to extend the length of any user’s mortgage at will while expanding their mortgage balance by only the minimum amount.
This results in users holding a mortgage for longer than they expect and requires them to pay more interest than initially promised.
Recommendation
Perform validation that the caller of the expandBalanceSheet function is the owner of the mortgage being expanded
-
M-06 Medium Large Penalty Payments Arbitraged Gaming Acknowledged
Description
The penalty payments made through the
loanManagerpenaltyPayfunction and the interest paid through the periodPay function both cause a stepwise increase in the value of consol tokens which can be taken advantage of, especially for large payments.An opportunistic actor can observe that a large penalty/interest is about to be paid, or have a large outstanding penalty/interest payment they themselves have to make, and frontrun this action to deposit a large amount of USDX into the consol. With a non-trivial share of the total supply of consol shares, the opportunistic actor can realize a significant portion of the penalty/interest payment made and effectively vampire attack this from other Consol depositors who held their shares while the penalty/interest was actually accrued over time.
The opportunistic actor may not be able to instantly withdraw their Consol to realize the immediate stepwise gain, however they profit this amount on paper immediately compared to the other Consol share holders who held for the entire time previous and now had their payment diluted. Furthermore, there may be cases where a large conversion is available through the conversion or forfeited assets queue which does allow the opportunistic actor to enter and exit consol in a very short period while gaining yield which should have accrued to other Consol holders.
Recommendation
There are many potential solutions to resolve this stepwise increase in share value issue. A fee for outside deposits into Consol could be added to disincentivize this behavior. The yield paid to the Consol contract could be spread out over time to avoid single stepwise jumps in share value which create such opportunities. Or a deposit delay could be implemented such that there is no guarantee that the actor is able to frontrun the interest/penalty payment value increase.
-
M-07 Medium Conversion Queue Perturbed Validation Resolved
Description
In the
expandBalanceSheetfunction there is no validation that requires the caller to provide a nonzeroconversionQueueaddress if that mortgage is currently enqueued, even though the mortgage must be re-enqueued into theConversionQueueat the new trigger price.If this mortgage was previously enqueued in the
ConversionQueueand is not re-enqueued in theConversionQueuethen within theprocessWithdrawalRequestsfunction the mortgage will be executed at the original trigger price when the mortgage was originally enqueued.This trigger price is not accurate to the trigger price of the new mortgage and in some cases may even reflect a loss for the mortgage’s average buy price, if there have not been frequent withdrawals processed, which can result in reverts which block the queue from progressing for an extended period of time.
Recommendation
If the mortgage being expanded in the
expandBalanceSheetfunction is already enqueued in theConversionQueue, require that the conversion queue address provided is nonzero and a valid conversion queue. -
M-08 Medium Borrowers Avoid Interest Through Consol Gaming Acknowledged
Description
When a large percentage of Consol holders have pending withdrawal requests in the various queues a large mortgage holder can recoup a large portion of their interest payments by simply depositing USDX into Consol and holding consol while they make their payments. The borrower can then process the queue entries as they are available so that each entry returns their share of the yield to the borrower’s Consol holdings.
In the case where a large mortgage exists and a significant portion of Consol holders are deposited in a queue with entries that can be processed or can be processed soon the mortgage owner can simply pay off their entire mortgage and then recoup the interest in a short period.
Recommendation
To disincentivize this behavior a fee could be levied directly to deposits made to Consol through USDX and outside of the Buttonwood system. Otherwise, there can be caps on the percentage of Consol that is allowed to be deposited into queues at any given time to limit the feasibility of this scenario.
-
M-09 Medium Consol Price Jumps After Queue Actions Frontrunning Acknowledged
Description
Throughout the codebase there are several invocations to
burnExcessSharesthroughout the various withdrawal queues. This function call creates a stepwise jump that re-distributes the yield from the holder who is withdrawing their tokens from the queue to the rest of the Consol holders.This can cause a significant stepwise jump in the Consol token price, especially for large Consol withdrawals which have spent a non-trivial amount of time in the queue. This is likely given the rate of interest payments made and length of mortgages in the Buttonwood cash protocol.
This stepwise jump can be arbitraged by other users who obtain consol right before this stepwise jump and lock in a guaranteed profit that otherwise they would have had to wait to accumulate honestly.
Recommendation
Instead of retro-actively burning the excess shares of the users who deposit into the withdrawal queue, consider removing them from the Consol token pool by withdrawing their assets from the Consol vault immediately upon their request to withdraw in the queue.
If the request is cancelled, then simply deposit these assets back into the Consol token at the updated share price.
-
M-10 Medium Re-enqueued Mortgages Trap Gas Fees Logical Error Resolved
Description
When dequeuing and re-enqueuing a mortgage a new gas fee is required to be covered by the user, however the gas fee provided for the original queue position is not recovered. As a result, this gas fee amount is left trapped in the
ConversionQueuecontract.Recommendation
Refund the gas fee collected by the old, queued mortgage and validate that the current gas fee has been provided.
-
M-11 Medium Incorrect MortgageId Update Logic Logical Error Resolved
Description
The
updateMortgageIdfunction is flawed because it does not remove the association of the previous mortgageId with the tokenId. Therefore, the tokenId is still associated with the old mortgageId in thegetTokenIdmapping.As a result, one tokenId could be used to occupy many common mortgageId’s, or these leftover
getTokenIdmapping values can cause confusion and issues for consumers of this mapping in normal use.Recommendation
Be sure to remove the
getTokenIdentry for the previous mortgageId in theupdateMortgageIdfunction. -
M-12 Medium Forclosures Used To Avoid Penalty Payments Gaming Acknowledged
Description
When forclosing a position into the forfeited assets queue the amount of forfeited asset tokens minted corresponds to the outstanding debt of the position without interest. It is one issue that this amount does not include interest.
However, this amount also does not include any penalty payments that the mortgage holder had outstanding. This makes sense from the third party Consol holder’s perspective. However, from the perspective of the mortgage holder, if they have accrued unpaid penalty payments up to this point then it makes sense for them to instead obtain Consol, enter the forfeited assets queue and allow their mortgage to forclose to redeem their underlying collateral without the penalty payments.
Recommendation
To protect against this potential gaming, consider setting aside the unpaid penalties from the collateral seized on forclosure and distributing it to all Consol holders similar to how would be done in the
penaltyPayfunction. -
M-13 Medium First Epoch Of All OriginationPools Can Be DoSed DoS Resolved
Description
The
deployOriginationPoolfunction does not have access control, and pools are deployed based on predetermined configs added by the admin.However, the function does not check if the config for the user-provided
oPoolConfigIdexists, allowing pools to be deployed with an empty config.A malicious actor can front-run the admin just before a new config is added, fetch the
oPoolConfigId, and deploy a pool before the config is stored. The resulting pool is unusable, as theConsolandUSDXaddresses are set to 0. SinceOriginationPooldeployments can only occur once per epoch, the initial epoch is effectively DoS'ed by this action, and the correct pool with a legitimate config can only be deployed in the next epoch.Recommendation
If the function is intended to be external, add a check to ensure that
config.consol != address(0)and verify that a valid config exists for the providedoPoolConfigId.Additionally, consider restricting the
deployOriginationPoolfunction to admin access only. -
M-14 Medium Collateral Value Not Considered For Foreclosures Logical Error Acknowledged
Description
Forclosures only occur if the mortgage owner misses more than
MAXIMUM_MISSED_PAYMENTSpayments. There is no check on the backing collateral value of the mortgage. When the market declines and the collateral value drop significantly, there is no incentive to continue making monthly payments to keep the collateral and redeem it later.Recommendation
Consider defining a safe ratio between the backing collateral value and the debt and allow mortgages to be foreclosed if the collateral value falls below that ratio, even if there are no missed payments. Additionally, allow borrowers to add more collateral to secure their loans.
-
M-15 Medium USDX Allows Slippage Free Swaps MEV Acknowledged
Description
The USDX contract is a
MultiTokenVaultwith no overrides on the deposit or withdrawal functions, and the conversion functions simply adjust the token amount by the relevant decimals. Therefore, asset A can be deposited and asset B can be withdrawn at exactly the same exchangeRate.This allows for a slippage free swap on the supported tokens, e.g. USDC and USDT. If one token depegs by even a small amount USDX would be immediately arbitraged to hold the depegged token at the cost of all USDX holders.
Recommendation
This could be addressed in many ways, including but not limited to adding an exchange rate mechanism for the tokens being deposited and withdrawn from USDX, adding a withdrawal fee that applies when withdrawals are made shortly after deposits, or requiring a cooldown period for withdrawals.
Otherwise acknowledge and be aware of the risk of arbitrage in the case where one token backing USDX depegs, or a user simply wishes to abuse a slippage free swap.
-
L-01 Low ForfeitedAssetPool Value Changes Drastically Logical Error Acknowledged
Description
When a mortgage is foreclosed, the entire remaining collateral is deposited into the forfeited assets pool, and an
amountOutstandingamount ofForfeitedAssetPooltokens is minted. Neither the mint amount nor the added collateral value is relevant to the current state of the forfeited assets pool. As a result, each foreclosure can significantly change the value of a singleForfeitedAssetsPooltoken.Users who want to withdraw from the ForfeitedAssetsPool burn their Consol tokens and enter the queue, expecting to get underlying assets from the pool based on the current value. However, there is no guarantee that the same amount of
ForfeitedAssetsPooltoken will be worth a similar value when this withdrawal request is processed, and users may end up withdrawing their Consol tokens at a much lower value than expected.Recommendation
Consider allowing users to provide a minimum value when requesting a withdrawal from the
ForfeitedAssetsPool, or clearly document this behavior, since users who enter theForfeitedAssetsQueueshould be aware of it. -
L-02 Low Some Lenders Don’t Receive Yield Unexpected Behavior Acknowledged
Description
Due to the structure of the USDX queue, some lenders who deposited into the origination pool and received Consol tokens will not be able to withdraw their principal or yield for a significant period of time.
Because a queue was chosen for this process, the lenders who submit their withdrawal requests in the USDX queue first will be fully redeemed, with interest, entirely first. And the ones who queue after them will have to wait until the final mortgages are fully repaid to redeem their principle and realize any interest.
As a result, some lenders will be able to withdraw within hours to days, while others will have to wait months to withdraw any amount of their Consol backing amount.
Recommendation
Consider restructuring the Consol redemption such that all Consol holders can withdraw a portion of their backing tokens as principal and interest is paid down over time, where this portion increases for all Consol holders evenly over time as payments are made.
-
L-03 Low Forfeited Assets Queue Zero Value DoS DoS Resolved
Description
The forfeited assets queue burn function iterates through all of the supported tokens and transfers them out to the burner relative to their holdings of the forfeited assets shares.
However, if any of the redeemedAssets tokens reverts on zero value transfers and the corresponding contract balance is empty, due to no forclosures occurring for that token, then a zero-value transfer revert will occur.
This will result in a stuck forfeited assets queue until a forclosure occurs for that asset or the admin sends some tokens to the contract to correct it.
Recommendation
Only perform the
safeTransfercall if the amount to transfer is nonzero. -
L-04 Low Cancelled Mortgages Retain The MortgageId Logical Error Resolved
Description
In the
burnfunction of theMortgageNFTcontract there is no clearing of thegetMortgageIdorgetTokenIdmappings for the relevanttokenId. Therefore, the tokenId continues to occupy the entries in these mappings and act as if it still existed.Recommendation
Remove the entries associated with the tokenId that is being burned in the
burnfunction. -
L-05 Low Withdrawal Queue Traps Yield Logical Error Acknowledged
Description
For the last holder of the Consol token the yield generated is trapped in the withdrawal queue contract and cannot be recovered by the protocol.
Consider the following series of actions using a withdrawal request and subsequent cancellation as an example:
- 100 total shares 100 total tokens
- Request withdrawal of 100 shares worth 100 tokens
- Withdrawal time: 100 shares worth 110 tokens, price per share = 1.1
- 100 total shares 110 total tokens
- SharesMinted = 100 * (100 - 100) / (110 - 100) > (100-100 totalShares is 0) > 100
- Shares - shares minted = 0
- No Adjustment for totalSupply or the sender’s balance
- Transfer(100)
- Convert to shares(100) > shares = 100 * 100 / 110 = 90.909 shares transferred
- 100 - 90.909 = 9.091 shares representing 10 tokens remain trapped in the withdrawal queue contract.
If the final Consol divestment is large this could result in a significant amount of funds trapped in the lending queue.
Recommendation
Consider adding a way to rescue any yield that could be trapped in the lending queue that would occur when the last Consol holder divests or cancels from the queue.
-
L-06 Low Interest Rate Used From Order Creation Validation Resolved
Description
In the
_prepareOrderfunction the interest rate for an order is fetched from thePythInterestRateOracle. However, the interest rate recorded from the oracle at the time of request creation may be significantly different than the current rate in effect when the mortgage is actually originated.Since there is no validation that the expiry is at most a certain amount in the future it’s possible that some mortgage requests could stick around in the
OrderPooland use stale interest rates when originated.Recommendation
Consider validating that the expiry is not too far in the future upon creation requests.
-
L-07 Low Forfeited Asset Pool Value Inflation Logical Error Acknowledged
Description
When assets are forfeited to the forfeited assets pool during foreclosure the
amountof outstanding debt is minted as forfeited asset shares, however the entire mortgage collateral is deposited into the pool.In cases where the mortgage position has already had payments made these changes the exchange rate of forfeited asset shares and allows the Consol holder at the front of the forfeited assets queue to withdraw more value than their consol should be worth.
For the most explicative example, consider a mortgage where all but 1 wei of the
termBalancehas been paid off, and the collateral is 10 BTC. The amount flash swapped from the Consol contract is simply 1 wei, and therefore the amount of new forfeited assets shares minted is just 1 wei. However, 10 BTC goes into the forfeited assets pool, and all of a sudden, the first Consol holder in the queue can redeem 1 wei of Consol for 10 BTC, while the queued redemptions behind them do not get the liquidity they should have to exit from this foreclosure.Recommendation
Either mint and transfer the current dollar amount of all collateral forfeited to the Consol contract, thus increasing the balance of all Consol holders and allowing them to exit fairly through the forfeited assets queue or consider converting between the amount of forfeited assets shares and underlying dollar value of collaterals in the forfeited assets pool so that Consol holders can be redeemed at a 1 USD value per Consol like the other exit queues.
-
L-08 Low Multiple OPool Origination Discrepency Unexpected Behavior Resolved
Description
The whitepaper suggests that multiple
OriginationPool’s can be used to originate a loan for an order, however the order fulfillment flow in theOrderPoolcontract only allows for oneOriginationPoolthat has been specified by the user to be used with the mortgage origination.This may limit the size of individual mortgages that can be created with a single request, and deviates from the whitepaper.
Recommendation
Confirm if this is the expected behavior or not, and if the whitepaper is simply outdated.
-
L-09 Low Collateral Caps Not Validated On Execution Validation Resolved
Description
The minimum and maximum caps for the chosen collateral token are validated upon a mortgage request but not upon the execution of that mortgage request.
Therefore, if these caps are updated in between the request and execution the resulting order execution may violate the most up to date caps.
Recommendation
Consider validating the minimum and maximum caps for an order during the execution of that order in addition to the request creation.
-
L-10 Low Expansion Requests Can Unexpectedly Fail Unexpected Behavior Resolved
Description
In the origination flow, if the order being executed is an expansion order the
reenqueueboolean is always marked as true when a conversion queue is provided.However, if the mortgage being expanded is not already in the conversion queue, then this enqueue request will fail with the
TokenIdNotInQueueerror.This leads to unexpectedly un-executable expansion orders which must be re-requested without a conversion queue and enqueued in a separate transaction.
Recommendation
Consider only re-enqueuing a mortgage if that mortgage is already present in the
ConversionQueue. -
L-11 Low Blacklisted Addresses May Halt Queues DoS Acknowledged
Description
The conversion queue transfers collateral tokens directly to the withdrawer’s address. As a result, if the withdrawer is blacklisted for the underlying collateral token, they can halt the withdrawal queue due to a revert which occurs when attempting to transfer to their blacklisted address.
A similar action can occur that prevents the Forfeited assets queue from functioning.
Recommendation
If the transfer fails in any queue, consider incrementing a mapping value whereby the withdrawer can claim the collateral token in a separate transaction. Otherwise, be sure to only list collateral tokens which do not have a blacklist mechanism.
-
L-12 Low Zero Payments Allowed Validation Resolved
Description
In the
periodPayandpenaltyPayfunctions there is no validation that prevents a 0-amount payment from occurring. This could lead to unexpected issues since a zero payment is not an expected operation. Furthermore, a minimum amount validation could be performed to add additional safety checks.Recommendation
Validate that the amount being paid in the
periodPayandpenaltyPayfunctions is nonzero and ideally introduce a minimum amount validation for these payments. -
L-13 Low Refinance Allowed For Completed Mortgages Validation Resolved
Description
The
refinanceMortgagefunction performs no validation that prevents a user from re-financing a mortgage that is nearly entirely paid off or entirely paid off already. This allows the user to create a mortgage with a term balance of 0 which can lead to unexpected edge cases in the protocol.Recommendation
Validate that the mortgage being refinanced has above a minimum threshold of debt before allowing the refinance to occur.
-
L-14 Low Conversion Queue May Get Stuck Warning Resolved
Description
In the
processWithdrawalRequestsfunction for the conversionQueue, thenumberOfRequestsis only decremented when a withdrawal request is fully fulfilled. Additionally, there is no maximum limit on the size of a withdrawal request.Therefore, for large withdrawal requests it is possible that the queue processing can be halted due to the invocation of the
processWithdrawalRequestsfunction with even a 0numberOfRequestsvalue. This is possible if there are many mortgages near the minimum size which can be matched against a single large withdrawal request, requiring that many loop iterations be completed before theprocessWithdrawalRequestsfunction can finish execution, which could use more than the block gas limit to fully process.Recommendation
To resolve this issue, consider implementing a maximum size for the individual withdrawal request amount, such that it is a multiple of the minimum mortgage size which can be processed in a single block.
Otherwise, adapt the usage of the
numberOfRequestsvalue in theprocessWithdrawalRequestsfunction such that it decrements upon every loop iteration, even when the withdrawal request is only partially processed and a mortgage is fully processed. -
L-15 Low Converted Positions Can Avoid Penalties Unexpected Behavior Acknowledged
Description
When a position is fully converted without making any period payments the
termBalancebecomes 0. As a result, the position no longer accrues late payments in the_applyPendingMissedPaymentsfunction and cannot be forclosed on as a result.For any position which had accrued penalty payments and then gets entirely converted they will not have any time constraint on paying these penalties as forclosure cannot occur.
Recommendation
The user still has to pay off these penalties to redeem their position, and this serves as the only incentive to do so. Be aware of this case where a mortgage cannot be forclosed on and penalty payments can be put off.
-
L-16 Low Creation Request DoS DoS Resolved
Description
The
requestMortgageCreationfunction accepts acreationRequestobject with amortgageIdNFT id attribute to mint the corresponding mortgage NFT id to the caller.However, if this
mortgageIdis already taken then themintinvocation and thus the higher levelrequestMortgageCreationfunction invocation reverts.As a result, it may be possible for a malicious actor to frontrun and DoS a user’s calls to the
requestMortgageCreationfunction by taking their specifiedmortgageIdfirst.Recommendation
Be aware of this and consider refactoring the mortgageId logic to be keyed based on the owner of the mortgage.
-
L-17 Low Yield Must Be Withdrawable From The Strategy Warning Acknowledged
Description
In the
SubConsolcontract there is no way to withdraw yield from the underlyingyieldStrategybeyond the collateral deposited into it. This may be unexpected depending on the intended use-case for theyieldStrategyand the targetyieldStrategyimplementations.Recommendation
Be sure there is a way to withdraw the yield from the yieldStrategy so that this amount is not lost.
-
L-18 Low Asset Removal Issues Warning Acknowledged
Description
In the case where an asset is being updated as no longer supported and has a nonzero balance in either the USDX or Consol contract a stepwise decrease in the vault shares value will occur.
In the case of Consol this creates a DoS for the queues which rely on the share price increasing monotonically due to the
burnExcessSharesfunction.Recommendation
Be aware of this risk and be sure to never delist an asset that has a nonzero balance in the Consol contract. It may be best to not explicitly limit this at a contract level to allow the admin to make important updates in emergency situations.
Furthermore, consider updating the
burnExcessSharesfunction so that it can successfully execute and early return if the share value has decreased since the withdrawal request was made. -
L-19 Low Missing SubConsol Validation Validation Resolved
Description
In the
requestMortgageCreationfunction there is no validation which prevents a user from providing a SubConsol address that is invalid either because it is not supported as a SubConsol address or it does not match the collateral token the user is using.This leads to orders being created which are inexecutable due to reverts in the
LoanManager.createMortagefunction.Recommendation
Add validation in the
requestMortgageCreationto prevent users from creating orders with a SubConsol address that is not a registered SubConsol contract and does not support the collateral they are providing. -
L-20 Low Lacking Configs Validation Validation Resolved
Description
In the
addConfigfunction there is no validation that prevents a config with a zero consol address from being added, such a config would be able to be added again, creating a duplicate in the_oPoolConfigIdslist and not allowing this config to be removed.Recommendation
Consider validating that the consol on the provided config is not the zero address.
-
L-21 Low Zero Amount Actions Allowed Validation Resolved
Description
Across the codebase in the
ForfeitedAssetsPool,Consol,USDX, andSubConsolcontracts 0 value deposits and withdrawals or burns are allowed which may lead to unexpected edge cases or event emissions.Recommendation
Validate that the amount being deposited or withdrawn or burned is nonzero to avoid unnecessary paths and unexpected edge cases.
-
L-22 Low Lacking Refund Mechanism Unexpected Behavior Resolved
Description
In the
GeneralManagerrequestMortgageCreationfunction among others, there is no ether value refund in the event that the value sent is greater than the order pool’s gas fee.In the event that the default admin re-assigns the gas fee value for the OrderPool before a user’s transaction is recorded, this may result in the user sending more gas than is necessary and not receiving an appropriate refund back.
Recommendation
Consider sending a refund of any excess ether to the caller at the end of the transaction.
-
L-23 Low Lacking SafeCast Usage Resolved
Description
In the
periodsSinceTermOriginationfunction theperiodsamount is casted to auint8without checking if the result fits within a uint8 object. This will result in a perturbment of the resulting periods value when the time passed exceeds 256 periods.In the
periodsPaidfunction the resulting value is casted to auint8without validating that the result fits within this storage.In the
interestRatefunction of the pyth interest rate oracle the rate result is casted to auint16without checking if it fits within this storage.In the
pricefunction the price result is casted to auint64value without checking if it fits within this storage.Recommendation
Use
safeCastfor all of these casting results as well as all other casting that occurs in the codebase to avoid any unexpected overflows. -
L-24 Low Convert Rounds In Favor Of The User Rounding Resolved
Description
In the
convertfunction, when a partial conversion is occurring the remainingtermPaidis calculated usingMath.Rounding.Ceil, however thetermPaidamount should be rounded down to round against the user and in favor of the protocol as a best practice.Recommendation
Round the
termPaidby the user down instead of up to favor the safety of the protocol. -
L-25 Low Lacking Expiration Validation Validation Resolved
Description
The
OrderPoolsendOrderfunction allows orders to be stored that have an expiration that has already passed.Recommendation
Do not allow these orders to be stored and instead revert.
-
L-26 Low Ineffective Mortgage Request Fills Unexpected Behavior Resolved
Description
The fulfiller of a mortgage origination receives no spread on the collateral price for fulfilling the order.
Furthermore, the price used is a static price from when the order was originated, therefore if price increases after the pyth price used at the time of the request initiation then it is unlikely that the order will be fulfilled, and this is simply a waste of the gas downpayment made by the user.
Recommendation
Consider improving the fulfillment incentives so that mortgage requests are more likely to be fulfilled.
-
L-27 Low Actions Can Only Occur During Market Hours Warning Resolved
Description
Given that the Pyth feed for the US treasury rate is only updated during relevant market hours it is not possible to query the rate with a recent published result during off-market hours.
As a result, actions like loan request creation and refinancing are unavailable during off-hours.
Recommendation
Be aware of this behavior and be sure to surface it to users in a palatable way.
-
L-28 Low Term And Penalty Payments Can Be Grieved DoS Resolved
Description
In the case where a borrower attempts to pay their full penalty or term value, a malicious user can cause the borrower's payment transaction to revert by frontrunning and paying the loan down by 1 wei.
The reason for the revert is because the borrower is not able to overpay their penalties or outstanding principal. The single-wei donation will cause the borrower's full payment to overpay by 1 wei, leading to their payment transaction reverting.
This is possible for all penalty payments and only period payments when the mortgage has a payment plan.
Recommendation
The simple solution is to lock down payments only to the mortgage owner. Otherwise, allow the borrower to overpay and simply refund the excess balance at the end of the transaction.
-
L-29 Low Check Balance In
removeAssetUnexpected Behavior ResolvedDescription
The
removeAssetfunction in theForfeitedAssetsPoolcontract removes the asset from the list without checking the contract balance. If this asset has already been forfeited before, that balance remains locked in the contract unless the asset is added back to the list, since a removed asset cannot be redeemed during a burn.Recommendation
Consider checking the contract balance in
removeAssetand either reverting if the asset has already been forfeited or clearing the balance. However, note that clearing the balance might be unfair to users who are awaiting withdrawal. -
L-30 Low USDX Withdrawals Can Be DoSed DoS Resolved
Description
The
USDX.solcontract allows users to deposit and withdraw various USD-valued tokens. Since the deposit and withdrawal flow does not impose fees, users are able to swap between tokens freely.This poses a problem because when users specify their withdrawal token, a malicious actor can deposit another token and withdraw all of the user's desired token's balance. All withdrawals can be temporarily grieved through this method.
Recommendation
Note this possibility in the documentation.
-
L-31 Low ForfeitedAssetsQueue Can Be DoSed DoS Acknowledged
Description
The ForfeitedAssetsQueue handles each withdrawal request sequentially and must fill have enough liability in the ForfeitedAssetsPool to be able to fulfill the request, otherwise it reverts.
Therefore, a large Consol holder can queue a withdrawal through the ForfeitedAssetsQueue and DoS the queue until there are enough forfeited assets to satisfy the large withdrawal.
The user could cancel their request resulting in simply stalling the distribution of forfeited assets and a delay in allowing users to exit their Consol positions.
This is unlikely to occur in practice because it would take a large financial commitment by the malicious user and provide little to no benefit.
Recommendation
Document the possibility of such a scenario.
-
L-32 Low Incorrect Natspec Comment Informational Resolved
Description
The
_findFirstTriggeredfunction has the following Natspec comment: "@return tokenId the tokenId of the first MortgagePosition in the Conversion Queue that has a trigger price greater than or equal to the input trigger price."However, the function returns the position with a trigger price less than or equal to the input trigger price.
Recommendation
Change the comment.
-
L-33 Low Inactive Mortgages In ConversionQueue Unexpected Behavior Resolved
Description
Redeemed or foreclosed mortgages remain in the
ConversionQueueuntil the dequeueMortgage function is called. If such a mortgage becomes the head of the queue, theprocessWithdrawalRequestsfunction will revert until that mortgage is removed.Recommendation
Consider either dequeuing a mortgage when it becomes inactive or skipping it in the while loop of the
processWithdrawalRequestsfunction. -
L-34 Low Old forfeitedAssetsPool Remains Supported Logical Error Resolved
Description
When updating the
forfeitedAssetsPoolinConsol.sol, the previous value offorfeitedAssetsPoolremains in thesupportedTokensenumerable set.The
capvalue for the previous pool is deleted, therefore it would be wise to remove the pool token from the list of supported tokens to avoid its use in theflashSwap()function.Recommendation
Consider removing the previous
forfeitedAssetsPoolfrom the enumerable set withsupportedTokens.remove(). -
L-35 Low Duplicate Deletion Of Order Error Resolved
Description
When an order is expired, the order record is deleted twice. Firstly, in the
ifclause:if (order.expiration < block.timestamp) { // Cancel the mortgage request IGeneralManager(generalManager).burnMortgageNFT(order.mortgageParams.tokenId); // Delete the order delete _orders[index]; // Emit the PurchaseOrderExpired event emit PurchaseOrderExpired(index);And finally at the end of function execution:
collectedGasFee = order.gasFee; // Delete the order delete _orders[index];Recommendation
Remove the deletion in the
ifclause as the order will eventually be deleted at the end of function execution. -
L-36 Low Early Closures Still Pay Full Interest Informational Acknowledged
Description
Borrowers may close their mortgages early if they choose. However, closing early does not recalculate the interest or total amount payable, and borrowers are still required to pay interest for the entire mortgage term.
Recommendation
Document this behavior and ensure users are aware of it.
-
L-37 Low Race Condition For ForfeitedAssetsPool Tokens Logical Error Acknowledged
Description
Consolis aMultiTokenVaultthat supports USDX andForfeitedAssetsPooltokens. The total supply ofMultiTokenVaults is calculated based on the balances of the supported tokens, andMultiTokenVaultassumes that all supported tokens share the same unit of account (UOA).However, the UOA of the supported tokens in Consol can differ significantly. For example, a single share of
ForfeitedAssetsPoolcan easily be worth 1.50 due to seized collaterals, while USDX is 1,00. As a result, Consol supporting both of these tokens contradicts the intended behavior of aMultiTokenVault.Since seized assets can be purchased at 1:1 by burning Consol, a race condition can occur when a single share of
ForfeitedAssetsPoolis worth more than $1. Users will attempt to withdraw from theForfeitedAssetsPoolfirst, followed by withdrawals from USDX.Recommendation
Consider documenting this if it is the intended behavior. Otherwise, ensure that all supported assets in Consol share the same UOA. This may require calculating the mint amount of
ForfeitedAssetsPooltokens at the time of foreclosure based on the value of the seized collateral and the value of Consol at that point. -
L-38 Low Penalty Calculation Rounds In Favor Of Borrower Rounding Resolved
Description
When calculating the number of penalties that the borrower owes, the penalty calculation rounds down in favor of the late borrower. Likely this only results in a negligible advantage for the late borrower; however, it is best practice to always round in favor of the protocol.
Recommendation
Use
Math.Rounding.Ceilto round in favor of the protocol. -
L-39 Low Initial
oPoolAdminDoes Not Have Admin Role Configuration ResolvedDescription
oPoolAdminin theOriginationPoolSchedulermust have theDEFAULT_ADMIN_ROLE. However, this role is not granted to oPoolAdmin during the initialization process. Instead, it is granted tomsg.sender, which may not be theoPoolAdmin.Recommendation
Grant the admin role to
oPoolAdminduring initialization. -
L-40 Low Penalty Rate Updates Affect Existing Mortgages Best Practices Acknowledged
Description
If the admin updates the penalty rate value stored in
GeneralManager.sol, the currently late mortgages could experience increased penalty fees. These mortgage owners enter into the agreement with a set rate and can experience fluctuations depending on the global penalty rate setting.Recommendation
Consider writing the current global
penaltyRatefor the mortgage at the time of origination request and use that value in subsequent penalty calculations. -
L-41 Low Theft Of Funds From USDX With scalarDenominator Rounding Resolved
Description
There is a possible, but unlikely theft vector within USDX.sol. The USDX contract allows users to deposit and withdraw various USD-valued tokens. Because each token may have different decimals, there is a scaling mechanism that multiplies the token amount by a numerator and divides by a denominator.
The potential theft vector occurs when the denominator is greater than 1, for example 10e12.
Imagine USDX is set up with 6-decimal precision and 18-decimal USD tokens must be scaled down to 6 decimals. When calculating the mint amount of 1e18 token, the resulting amount would be 1e6.
When the user attempted to withdraw 1e18 tokens, the same conversion would take place and require that they burn 1e6 pool tokens.
The theft can occur if the user specifies that they wish to withdraw (1e18 + 1e11) tokens. With precision loss, the amount of pool tokens to burn will still be 1e6.
However, the user will receive 1e11 tokens more than they deposited. They can perform this call many times to drain funds from the pool.
Recommendation
Ensure that the denominator is always 1, as is the case in the deployment script.
-
L-42 Low CEI Pattern Not Followed Best Practices Resolved
Description
When cancelling a withdraw request, the consol token is transferred to the user, however, the amount is not updated to 0 until after the external call. If Consol were to include a hook for the receiver of the token, the withdrawal queue could be drained. It is unlikely that such would be the case.
Recommendation
Move the storage writes above the external call to the consol token.
-
I-01 Informational calculateNewAvergageInterestRate Naming Typo Resolved
Description
The
calculateNewAvergageInterestRatefunction misspells average.Recommendation
Correct the spelling of average in the
calculateNewAvergageInterestRatefunction name. -
I-02 Informational Typos Informational Resolved
Description
- Corresponding -> a Guardian proof of concept
- Surplus -> a Guardian proof of concept
- Additional -> a Guardian proof of concept
Recommendation
Consider, fixing these typos.
Remediation Review
35 findings · September 13 to 28, 2025-
H-01 High Some Requests Cannot Be Cancelled Logical Error Resolved
Description
In the cancelWithdrawal function the validation on the index ensures that the index is less than the withdrawalQueueLength. However, the withdrawalQueueLength is variable and the indexes are now fixed.
Therefore, there can be a withdrawal at absolute index 10, while the withdrawal head is at 10 and the withdrawal queue length is 1. The index provided to the cancelWithdrawal function must be 10, which is greater than the withdrawalQueueLength of 1.
Consider the following example:
- The withdrawal queue is empty with the head being 0 and length being 0
- Bob creates withdrawal A, which is stored at index 0, the head is still 0 and length is now 1
- Alice creates withdrawal B, which is stored at index 1, the head is still 0 and length is now 2
- Bob’s withdrawal A is now processed; the head is at 1 and the length is now 1
- Alice attempts to cancel her withdrawal, however providing the index 1 fails the index validation and she cannot cancel
Furthermore, the current validation allows requests that are before the head to be cancelled, however these requests have already been processed.
Recommendation
Correct the index validation such that only indexes that satisfy
head <= index < head + lengthare allowed. -
H-02 High amountForfeited Credits Mortgage Holders Logical Error Resolved
Description
The amountConverted variable has been changed to represent the amount of debt converted in past terms. And the current term converted value is now tracked in the termConverted variable. This subtly changes all formulas that currently use amountConverted, and if termConverted is not also tracked there this causes notable accounting issues.
The amountForfeited function uses amountConverted but does not account for the newly introduced termConverted correctly.
The original implementation of amountForfeited was as follows:
amountBorrowed - amountConverted - amountOutstanding
Where amountOutstanding = amountBorrowed - amountConverted - amountPrior - convertToPrincipal(termPaid)
And in this context amountConverted represents the entire amount converted across all terms for the mortgage.
This simplifies to:
amountBorrowed - amountConverted - amountOutstanding
= amountBorrowed - amountConverted - (amountBorrowed - amountConverted - amountPrior - convertToPrincipal(termPaid))
= amountPrior + convertToPrincipal(termPaid)
This is correct because this is the amount of debt that the user has already paid and is forfeiting during liquidation.
However, the new implementation of amountForfeited is as follows:
amountBorrowed - amountConverted - principalRemaining
Where principalRemaining = amountBorrowed - amountConverted - amountPrior - convertToPrincipal(termPaid + termConverted)
And in this context amountConverted represents the entire amount converted across only past terms for the mortgage.
This simplifies to:
amountBorrowed - amountConverted - principalRemaining
= amountBorrowed - amountConverted - (amountBorrowed - amountConverted - amountPrior - convertToPrincipal(termPaid + termConverted))
= amountPrior + convertToPrincipal(termPaid + termConverted)
This is incorrect because now the mortgage holder is credited with forfeiting the converted amount in the current term.
Recommendation
Update the forfeitedAssets function as follows:
function amountForfeited(MortgagePosition memory mortgagePosition) internal pure returns (uint256) { if (mortgagePosition.status != MortgageStatus.FORECLOSED) { return 0; } - return mortgagePosition.amountBorrowed - mortgagePosition.amountConverted - mortgagePosition.principalRemaining(); + return mortgagePosition.amountPrior + mortgagePosition.convertPaymentToPrincipal(mortgagePosition.termPaid); } -
H-03 High Gas Fees Trapped In enqueueMortgage Logical Error Resolved
Description
The
enqueueMortgagefunction uses the_calculateRequiredGasFeefunction to calculate themsg.valuethat is required to send to all of the conversion queues being added with theconversionQueueListparameter.However, the
_calculateRequiredGasFeefunction computes the required gas fees for all conversion queues that the tokenId currently has in theirconversionQueues(tokenId)list, not just the ones that are being added with the call to enqueueMortgage.Therefore, the
requiredGasFeeis larger than what is necessary to send to the new conversion queues, and this amount is lost to the mortgage owner.Recommendation
Instead of using the
_calculateRequiredGasFeefunction which computes the necessary gas fee for all of the conversion queues, old and new. Implement a bespoke for-loop to loop through all of the new conversion queues in the providedconversionQueueListparameter. -
H-04 High Missed Payments Perturbed Logical Error Resolved
Description
In the
applyPenaltiesfunction, for mortgages that do not have a payment plan, theadditionalPaymentsMissedare assigned to:periodsSinceOrigination - mortgagePosition.totalPeriods + 1 - mortgagePosition.paymentsMissedHowever, in the
periodPayandconvertfunctions, themortgagePosition.paymentsMissedvalue is assigned to the following for mortgages with and without payment plans:mortgagePosition.paymentsMissed = _periodsPaid > periodsSinceOrigination ? 0 : periodsSinceOrigination - _periodsPaid;There are several issues with this, the most glaring being that in conversion the paymentsMissed update does not account for the fact that mortgages without a payment plan cannot have missed payments before their mortgage is complete. This is not an issue in the
periodPayfunction since mortgages without payment plans must be totally paid off when invoking theperiodPayfunction.Secondly, if the
periodsSinceOriginationis larger than the total mortgage periods then thepaymentsMissedwill always be assigned to a nonzero amount even if the total debt has been paid off. The paymentsMissed logic should be updated like so:uint8 periodsSinceOrigination = mortgagePosition.periodsSinceTermOrigination(latePenaltyWindow); + uint8 cappedPeriodsPassed = Math.min(periodsSinceOrigination, mortgage.totalPeriods); uint8 _periodsPaid = mortgagePosition.periodsPaid(); - mortgagePosition.paymentsMissed = _periodsPaid > periodsSinceOrigination ? 0 : periodsSinceOrigination - _periodsPaid; + mortgagePosition.paymentsMissed = _periodsPaid > cappedPeriodsPassed ? 0 : cappedPeriodsPassed - _periodsPaid;Thirdly, both the
periodPayfunction and theconvertfunction do not account for the+ 1that is applied to theadditionalPaymentsMissedin the applyPenalties function and therefore overwrite this additional missed payment period for mortgages that do not have a payment plan.Recommendation
Firstly, in the
convertfunction, be sure to account for the fact that mortgages without a payment plan cannot have missed payments before their term is up.Secondly, account for the additional payment period that is applied to non-payment-plan mortgages in the
periodPayandconvertfunctions. -
H-05 High Rounding Prevents Action Cancellations Rounding Resolved
Description
In the
cancelWithdrawalfunction in some cases the number of shares being transferred for therequest.amountwill round to be one wei greater than the number of shares that theLenderQueueholds for that withdrawal request after theburnExcessSharesinvocation.In cases where there is only one withdrawal request in the
LenderQueue, the cancellation will revert due toERC20InsufficientBalance, and in the case where there are multiple withdrawal requests, the cancellation will take 1 wei that should have been allocated to other withdrawal requests, ultimately causing a revert for the last withdrawal being cancelled or even processed.Recommendation
The rounding of
burnExcessSharesand the subsequent transfer must be overhauled to avoid sending an additional wei. A simple solution may be to transferrequest.amount - 1to ensure this case does not arise. -
H-06 High Multiple Origination Pools Broken Logical Error Resolved
Description
The latest logic in the origination flow sends the available consol token balance of the GeneralManager contract to the immediate Origination pool being processed.
IERC20($._consol).safeTransfer(_msgSender(), IConsol($._consol).balanceOf(address(this)));This however leaves an insufficient amount of consol tokens to repay the following origination pools. This is masked by the following logic:
uint256 consolBalance = IConsol($._consol).balanceOf(address(this)); if (consolBalance < returnAmount) { IConsol($._consol).deposit($._usdx, IConsol($._consol).convertUnderlying($._usdx, returnAmount - consolBalance)); }Which will take from the USDX balance of the contract to repay the following origination pools. This is not surfaced immediately as an error because the fulfiller now receives the balance of the contract instead of the purchaseAmount they should have received.
Recommendation
Instead of transferring the entire GeneralManager balance to the immediate origination pool being processed, transfer the specified returnAmount.
This way there still may be small rounding which affects the fulfiller, but it is not confused with an amount that covers a dearth for other origination pools.
-
M-01 Medium Mortgage Fee Lost On Expiration Warning Resolved
Description
In the
OrderPoolcontract the executor of theprocessOrdersfunction has an adverse incentive to wait until orders are expired before processing them, this is because theorder.mortgageGasFeeis added to thecollectedGasFeein_processOrderwhen an order has expired.This means that when an order expires the user loses their
mortgageGasFeeeven though a mortgage was not created, and the executor gets to keep it for themselves.Recommendation
If an order expires, still collect the
order.orderPoolGasFee, however theorder.mortgageGasFeeshould be refunded to the user. It should be refunded by incrementing a mapping value entry and allowing the user to claim this Ether in a separate transaction to avoid any DoS vectors or gas griefing vectors. Or by wrapping the Ether to wrapped Ether which does not allow for these attacks. -
M-02 Medium Risk Free Time Difference Arbitrage Gaming Acknowledged
Description
When a mortgage creation request is created, the purchaseAmount and collateralAmount are defined based on the price of the asset using the pyth oracle in the block of the creation request.
The creation request is then executed up to 5 minutes later, using these prices from as old as 5 minutes ago.
This allows users to potentially arbitrage price movements which occur in those 5 minute timeframe, by deciding whether or not their order is executed.
For example:
- At time 100, User A makes their expand balance sheet request when Bitcoin is $100,000
- User A configures their creation request to enqueue into a conversion queue that they are already enqueued in
- User A sets their receive function to revert based on a boolean switch, which is currently set to true to prevent the order from being executed
- At time 110, Bitcoin rises to $110,000
- User A sees that Bitcoin price has moved in their favor and flips the boolean switch allowing the receive function to no longer revert and the refund that occurs on the re-enqueue action to proceed
- User A’s action is now processed using an outdated price of $100,000 per Bitcoin for their borrow
- If price had not moved in User A’s favor, then they would have just left the switch and allowed their order to be cancelled.
Recommendation
Be aware of this time difference arbitrage risk, the OrderPool execution fee may be enough to disincentivize this, furthermore the net interest rate paid on a mortgage will always overshadow what can be arbitraged through this method in the current expiration window.
-
M-03 Medium Collateral Consumed At Stale Trigger Price Logical Error Acknowledged
Description
When processing withdrawals,
collateralToUseis computed by dividing the payment by the storedtriggerPricerather than the current oracle price. If the market price has risen above the trigger price, the system will consume more collateral than necessary for the same repayment.While the borrower accepted the trigger price for conversion, this should not mean that the borrower converts their collateral at a rate lower than its market value. Additionally, the comment in the code states: 'Figure out how much collateral corresponds to the amountToUse at the current price.'
Recommendation
Consider using the current value of the collateral during conversions, as in the previous version. However, if this change is an intentional design choice, clearly document this behavior for users, since they will convert their collateral at a lower rate unless the trigger price matches the current value. In that case, also update the comment accordingly.
-
M-04 Medium Converted No Payment Plan Mortgages Logical Error Resolved
Description
In the
periodPayfunction validation is performed on mortgages with no payment plan to ensure that the amount is at least themortgagePosition.termBalance.However, for mortgages with no payment plan that have been partially converted, there isn’t enough debt to pay off the entire
termBalance, since thetermConvertedis reducing a portion of this debt.Due to the refund logic this doesn’t strictly prevent these mortgages from being paid off, however users must obtain and approve more than their actual debt amount of Consol to pay off their remaining debt in this case.
Recommendation
Update the validation to:
if (!mortgagePosition.hasPaymentPlan && amount < mortgagePosition.termRemaining()) { revert CannotPartialPrepay(mortgagePosition); } -
M-05 Medium No Payment Plan Mortgages Arbitraged Gaming Acknowledged
Description
Mortgages without payment plans require that their entire debt is paid off in a single
periodPayinvocation.However, these mortgages in particular, create a large arbitrage opportunity when they pay back their debt. Where the forfeited Consol that pays the interest on the loan is forfeited, increasing the Consol price.
This may be abused by third parties by depositing into USDX, then obtaining Consol tokens right before the large payment and then immediately exiting one of the available queues, likely the USDX queue using their own deposited USDX and the mortgage holders USDX, as long as this queue is not too full.
Furthermore, this may serve as an avenue for the mortgage holder to avoid paying interest. If they are able to flash loan or make a short period loan of a large amount of Consol tokens, they themselves could absorb much of the Consol price increase and re-coup their interest payment.
Recommendation
Be aware of this heightened risk of gaming, particularly for mortgages without a payment plan.
-
L-01 Low Empty Requests Can Be Cancelled Validation Resolved
Description
In the
cancelWithdrawalfunction there is no validation that prevents an empty, already cancelled, withdrawal from being processed again.There is no net effect of processing such a withdrawal, however this execution path should be limited to avoid unexpected outcomes and the emission of a misleading
WithdrawalCancelledevent.Recommendation
Validate that either of the request shares and amount must be nonzero to allow processing to occur.
-
L-02 Low safeTransferFrom Should Come First Best Practices Acknowledged
Description
In the
MultiTokenVaultdepositfunction thesafeTransferFromaction comes after the minting of tokens to the user, therefore the user receives shares before officially paying for them.To avoid any unexpected re-entrancies with arbitrary supported tokens, the
safeTransferFromshould occur before the_mintaction so there is no point where the protocol is in an invalid state at the time of an external call.Recommendation
Perform the
safeTransferFromaction before the mint action in thedepositfunction. -
L-03 Low More Than 18 Decimals Incompatible Validation Acknowledged
Description
In the
addSupportedTokenfunction validation has been added to ensure that thescalarNumeratoris greater than or equal to thescalarDenominator.This disallows tokens with greater than 18 decimals from being used with the system, such as yamV2 which has 24 decimals.
Recommendation
Be aware of this incompatibility or consider removing the validation.
-
L-04 Low Lacking CEI In Burn Best Practices Resolved
Description
In the USDX burn function the USDX burn occurs for the user at the end of the function after all tokens have been redeemed to the user. However, to follow CEI the burn should occur first.
Recommendation
Invoke _burn after initial validations in the burn function.
-
L-05 Low Dangerous Withdrawal Request Amounts Validation Acknowledged
Description
In the requestWithdrawal function if the amount is a few wei the action may produce a withdrawalRequest that holds a nonzero amount value but a zero shares value.
This creates an unexpected case in the queues that attempt to process such withdrawals.
Furthermore, if the amount specified is zero, this allows users to create withdrawals with both zero-amount value and zero share value.
Recommendation
Both of these cases are disallowed when the minimumWithdrawalAmount is assigned to a non-trivial value. Ensure that the minimumWithdrawalAmount is always assigned to a minimum reasonable amount to ensure no game-ability and rounding issues are present in all queues.
-
L-06 Low Lacking Pausability Guards Validation Resolved
Description
The
ConversionQueuehas awhenNotPausedguard which applies to theprocessWithdrawalRequestsfunction, however theUSDXQueueandForfeitedAssetsQueuedo not implement such a validation.Recommendation
Consider if the
USDXQueueandForfeitedAssetsQueueshould use a whenNotPaused modifier for theirprocessWithdrawalRequestsfunctions. -
L-07 Low Missing Reentrancy Guard Validation Resolved
Description
In the
OrderPooltheprocessOrdersfunction is the only function which has anonReentrantmodifier which provides limited protection against reentrancy into theprocessOrdersfunction.The
sendOrderfunction still poses a risk of unexpected reentrancy and therefore should also use thenonReentrantmodifier so that it cannot be unexpectedly entered into during aprocessOrderexecution.Recommendation
Add a
nonReentrantguard to thesendOrderfunction. -
L-08 Low Blacklist Prevents Order Processing Warning Acknowledged
Description
In the case that an order is expired, the collected collateral and USDX are returned to the user and the executor gets to collect the associated gas fee.
However, if the user happens to be blacklisted for the collateral token, the
_processOrderfunction will not be able to execute and process their order as it attempts to send tokens to a blacklisted address.Recommendation
Be aware of this in the event that any supported collateral tokens implement a blacklist. Consider using a mapping to track balances that a user may claim in a separate transaction rather than pushing assets to the user.
-
L-09 Low Lacking Processing CEI Warning Resolved
Description
In the
_processOrderfunction the_ordersmapping is cleared at the end of the function, however, to follow CEI it would be best if this mapping entry is cleared at the beginning of the function.Recommendation
Clear the
_ordersmapping at the beginning of the_processOrderfunction directly after caching the order in memory. -
L-10 Low Lacking Length Validation Validation Resolved
Description
When creating a mortgage request the
collateralAmountslength must match theoriginationPoolslength, however this is not validated in therequestMortgageCreationfunction.Recommendation
Add validation so that the
collateralAmountslength is required to match theoriginationPoolslength. -
L-11 Low Outdated Gas Fee Validation Validation Acknowledged
Description
In the
OrderPoolcontractsendOrderfunction the validation performed on themsg.valueis still against thegasFee, however thegasFeeonly represents the order pool gas fee and does not include the mortgage gas fee that should be provided.Recommendation
Update the msg.value validation in the
sendOrderfunction so that it includes the_calculateMortgageGasFeeresult. -
L-12 Low Typo Typo Resolved
Description
A comment in the
originationPoolDeployCallbackfunction states:// Send in the collateral to the LoanManager before creating the origination poolHowever, this should read:
// Send in the collateral to the LoanManager before creating the mortgageRecommendation
Correct the comment.
-
L-13 Low Dangerous Conversion Queue Refund Best Practices Resolved
Description
When enqueuing a mortgage in the
ConversionQueue, if the mortgage was previously enqueued it is first removed from the queue to be re-enqueued. In this process the original gas fee is returned to the user, however this allows the user to execute arbitrary action in their receive function.This may cause unexpected issues due to the user being able to DoS, gas grief the executor, or re-enter into the system unexpectedly.
Recommendation
Instead of sending Ether directly to the user, consider incrementing a mapping value where they can claim their owed Ether in a separate transaction. Otherwise wrap the Ether and send it to the user as to avoid triggering untrusted code.
-
L-14 Low Lacking Individual Borrow Validation Validation Acknowledged
Description
In the
_validateBorrowCapsfunction there are some baseline validations that the borrowed amounts are within a set range. However, there is no validation that the individual borrows amounts in eachoriginationParameters.borrowAmountslist entry are within an expected range.Therefore, while the total may be inside the expected range, the amount requested from each origination pool may be unexpected.
Recommendation
Consider validating that each origination pool requested amount is above some minimum expected amount, or at least above 0.
-
L-15 Low Duplicate Origination Pools Validation Resolved
Description
Now multiple origination pools are allowed to be used for an origination of a mortgage. However, there is no validation to ensure that each entry of the
originationParameters.originationPoolslist is unique upon creation of the request.Recommendation
Consider validating that each entry is unique when a mortgage request is being created.
-
L-16 Low Lacking Reentrancy Guards Validation Resolved
Description
Throughout the
GeneralManagerthere are important functions which could be reentered into which are lacking reentrancy guards.These functions include:
requestMortgageCreationrequestBalanceSheetExpansionoriginateenqueueMortgageconvert
Recommendation
Add the
nonReentrantmodifier to these functions. -
L-17 Low Unused subConsol Variable Superfluous Code Resolved
Description
In the
ConversionQueuecontract thesubConsolstorage variable is unused.Recommendation
Consider removing it.
-
L-18 Low Incorrect NatSpec Comment Informational Resolved
Description
/** * @title QueueProcessor * @author SocksNFlops * @notice The StaticInterestRateOracle contract is a contract that returns a static interest rate for new Mortgages being originated. //@audit-issue L incorrect Natspec comment */The
@noticecomment inQueueProcessorcontract belongs toStaticInterestRateOracle.Recommendation
Update the comment.
-
L-19 Low Redundant Check In
convertSuperfluous Code AcknowledgedDescription
The
convertfunction in the MortgageMath library checks that the current price is greater than theconversionTriggerPrice.if (mortgagePosition.conversionTriggerPrice() > currentPrice) { revert ConversionTriggerPriceNotMet(mortgagePosition, currentPrice); }However, the same check is performed again at line 673, making it redundant.
Recommendation
Remove the redundant check.
-
L-20 Low Redundant Values Logical Error Acknowledged
Description
When a mortgage is created with a conversion queue or enqueued later, the
_conversionQueuesand_mortgageEnqueuedmappings are populated. However, these mappings are never cleared, even though the mortgage is eventually fully converted.A fully converted mortgage is removed from its respective
ConversionQueueduring the_popMortgagecall. However, it still remains in otherConversionQueues and must be popped from these queues during withdrawal request processing. Additionally, it remains in theGeneralManagermappings as if it were still in the queue.Recommendation
Consider updating or clearing the
_conversionQueuesand_mortgageEnqueuedmappings inGeneralManagerwhen a mortgage is fully converted. However, care must be taken to ensure that this does not cause a DoS when the mortgage still needs to be popped from otherConversionQueues during loop iterations. -
L-21 Low Forfeited Assets Queue Held Up Warning Acknowledged
Description
The
ForfeitedAssetsQueuecan be held up by large withdrawal requests which cannot be fully processed because the Consol holdings have not acquired enoughForfeitedAssetsPooltokens to withdraw.This can reasonably occur especially for foreclosures where the remaining debt of the position is small at the time of foreclosure, in which case it would require many such foreclosures to occur to allow the forfeited assets queue to fill a significant withdrawal in the queue.
Recommendation
In future iterations consider allowing partial withdrawals. Otherwise, be aware of this blocking behavior and communicate it with users.
-
L-22 Low Missing requestWithdrawal Refund Warning Acknowledged
Description
The
requestWithdrawalfunction does not implement a refund mechanism of too much ether is sent, this does not match other functions in theGeneralManagerthat do perform refunds for native ETH.Recommendation
Consider introducing a refund for the
requestWithdrawalfunction in future iterations. -
L-23 Low Incorrect Event Emission Logical Error Resolved
Description
In the
burnfunction the Withdraw event for the final withdrawal is intended to have the remaining amount - totalBurned, however this case is entered for all withdrawals that come before the final withdrawal, because the main if case isi == supportedTokens.length() - 1, meaning that the last withdrawal goes into the first if branch.Recommendation
Update the if case to use the condition
i != supportedTokens.length() - 1. -
L-24 Low Count Unexpectedly Incremented Twice Unexpected Behavior Resolved
Description
In the
ConversionQueue, the count variable which is meant to track iterations of the processing loop can unexpectedly be incremented twice in a single loop iteration when theamountToUsematches both therequest.amountand themortgagePosition.principalRemaining().This may be unexpected for the caller of the processWithdrawalRequests and may result in less withdrawal requests being processed than expected.
Recommendation
Be aware of this edge case and inform callers of the
processWithdrawalRequestsfunction.
Remediation Review 2
1 finding · September 24, 2025-
H-01 High Multiple Origination Pools Broken Logical Error Acknowledged
Description
The latest logic in the origination flow sends the available consol token balance of the GeneralManager contract to the immediate Origination pool being processed.
IERC20($._consol).safeTransfer(_msgSender(), IConsol($._consol).balanceOf(address(this)));This however leaves an insufficient amount of consol tokens to repay the following origination pools. This is masked by the following logic:
uint256 consolBalance = IConsol($._consol).balanceOf(address(this)); if (consolBalance < returnAmount) { IConsol($._consol).deposit($._usdx, IConsol($._consol).convertUnderlying($._usdx, returnAmount - consolBalance)); }Which will take from the USDX balance of the contract to repay the following origination pools. This is not surfaced immediately as an error because the fulfiller now receives the balance of the contract instead of the purchaseAmount they should have received.
Recommendation
Instead of transferring the entire GeneralManager balance to the immediate origination pool being processed, transfer the specified returnAmount.
This way there still may be small rounding which affects the fulfiller, but it is not confused with an amount that covers a dearth for other origination pools.
No findings match.
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.
