Audit · firm’s term: audit report
QuillAudits reviewed Aconomy Marketplace (Aconomy) on 24 Jun 2024: 37 findings, 0 critical; 20 marked fixed, 17 open.
Report hosted on quillaudits.com.
Trustblock does not review code.
Statuses are the security firm’s own, from its report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 0 |
| Medium | 4 | 0 | 3 |
| Low | 13 | 0 | 17 |
stake() and addStake() allows anyone to stake any ERC20 token. In case the ERC20 token deducts fees on transfer, the calculation while storing/incrementing the stakedAmount would be incorrect.E.g. The Validator stakes 100 tokens, the stakedAmount for that validator increased by 100 on L63. But while transferring tokens on L65 only 90 tokens were transferred (as 10 tokens deducted as some tax/fee by token contract). In this case, the increase of 100 on L63 is incorrect. So this can trick the owner into refunding 100 tokens to the validator address even though less than 100 tokens were transferred. (It should be noted that while transferring the tokens back to the validator with refundStake(), less will be transferred in case of fee deducting tokens.) In piNFTMethods.addERC20() allows validators to add any ERC20 token. In case the ERC20 token deducts fees on transfer, the calculation in updateERC20() for adding _value in erc20Balances mapping would be incorrect. There’s a similar possibility in both piMarket and NFTlendingBorrowing regarding the usage of fees deducting tokens. While this issue can be rare, we have decided to highlight the issue as there would be unpredictable possibilities for an open system like this when it comes to the types and incorrect implementations of ERC20 tokens.
The isLoanDefaulted() function is currently unused in the codebase, while in an isolated event this wouldn't be an issue but the poolAddress contract has no checks for borrowers who default on loans. As it stands, nothing stands between a defaulting borrower and a new loan being approved - not even with the scrutiny of the proposed lender. Within the loanRequest() function, there isn't any check/flag that indicates the borrower submitting a loan request has any bad debt. This isn't taken into consideration when the lender calls AcceptLoan(). The borrower's active loans are added to when in the actual sense, previous loans could be past their expiration time, thus defeating the purpose of a liquidation which occurs when liquidators (who are usually incentivized) clear bad debts from the system. In both FundingPool and poolAddress the precaution needs to be taken if the borrower never returns the taken loan back.
When the lender supplies the amount to the pool with supplyToPool() it's getting checked that the _amount is greater or equal to 1000000.But when the poolOwner tries to repay the monthly amount after accepting the lender's bid, in the repayMonthlyInstallment() on L528 it's getting checked that fundDetail.amount / fundDetail.installment.installments greater or equal to 1000000. Let's say the lender entered _amount as 1000000 for the supplyToPool(), the check on L184 passed as the _amount is equal to 1000000. Then while paying the monthly installment on L528 it will revert as in this case fundDetail.amount / fundDetail.installment.installments would be 1e6/6 = 166666, which is less than 1000000. Resulting in denial of service while paying the monthly amount. While there can be an alternate solution to repay the full amount, the monthly repayment functionality would be useless. A similar situation occurs in poolAddress.sol with the loanRequest amount being checked to be greater or equal to 1000000, and repayMonthlyInstallment checks that the principal divided by the number of installments is greater or equal to 1000000 (1e6). If the borrower takes a loan worth 1000000 (1e6) tokens, he would be unable to repay the monthly
The users can take non collateralized loans in the Fundingpool and poolAddress. The ability to take non collateralized loans creates the possibility where the borrowers may never return the loans.
piNFT and validatedNFT allow functionality to mint NFTs. both piNFT.mintNFT() and validatedNFT.mintValidatedNFT() allows anyone to mint the NFTs.Assuming that these NFTs represent the on-chain presence of real-world tokens, allowing anyone to create/mint the tokens on-chain through these contracts may affect the business logic.
There is a possible scenario where the lender adds the bid with short bidDetail.expiration and can claim NFT directly after the block.timestamp goes ahead of bidDetail.expiration. So here the borrower gets less (or say inconsistent) time frame to pay the loan and interest amount.In this case showing the timeframe on frontend in which the borrower needs to pay the loan would be helpful, so that the borrower would be aware while/before accepting the loan that after the block.timestamp goes ahead of bidDetail.expiration the lender can claim his(borrower's) NFT.
In FundingPool on L504 if (monthsSinceStart > lastPaymentCycle) is true then it returns the fundDetail.paymentCycleAmount that would be the amount for the 1 cycle/month but it can happen that more than one months are elapsed after the last repayment in this case the amount needs to be adjusted accordingly.The same thing is happening in the poolAddress’s viewInstallmentAmount().
Nft is not getting transferred from the NFT owner to the contract when the validator is adding ERC20 with addERC20(). In this case the validator needs to trust the NFT owner, NFT owner may decide to not call redeemOrBurnPiNFT() assuming that the user is already the owner of NFT.
As a way of identifying irregularities or where certain requirements are not met, errors are logged in smart contracts. Many of the require statements currently do not have error messages attached to them. Functions could revert without the user knowing/receiving adequate feedback. Adding error messages can also help while handling exceptions on the front end.
Instead of attestation functionality simple mapping for address to boolean can be used to verify lender and borrower addresses in the poolRegistry. In this way the functionality would be more simple to understand and gas efficient. Eventually the one who can add the lender and borrower would be the same address i.e. pool owner. In turn it will reduce the attack surface.
In both FundingPool.sol, poolAddress.sol the calculateNextDueDate() calculates the very next due date without taking the current timestamp in account. So it can happen that according to the current timestamp more than 1 due date has elapsed but it will still calculate the very next due date from the last repaid timestamp.
The lenders can check the added amount for specific piNFT for which they are bidding. Doing so helps the lenders to get some assurance that even if the borrower is unable to repay the taken loan+interest, the borrower’s NFT can be claimed and this situation can be settled with the validator in real world and or with withdrawing the ERC20 from piNFTMethods.In the case the borrower is the owner of NFT with no ERC20 added for his NFT in the piNFTMethods, the lenders wouldn’t be able to get the assurance that they will get in the case when the borrower would be the user with ERC20 value added to his piNFT. And in the case of the borrower being unable to pay the loan, the lender has no options to settle this situation. Additionally it should be noted that the NFTlendingBorrowing allows users to list the other NFTs apart from the piNFT.
In PoolRegistry.sol#L384, _attestAddress() has a require check that is repeated in the public functions (addLender, addBorrower) that call this internal function. The public functions have the ownsPool modifier which performs the exact same check. This will make the function calls to add lenders and borrowers to the protocol consume more gas than required.Same happens in poolAddress.sol#L405, where the private _repayFullLoan() function has the same require(loans[_loanId].state == LoanState.ACCEPTED); check as repayMonthlyInstalment().
In addERC20(), the code checks that the validator is adding ERC20 that is equal to what was added for that collection and token previously,if different then it reverts on L207.This means the address type variable can be used in the erc20Contracts mapping instead of address array at the last, as it needs to only hold the one address. This in turn reduces other logic like the logic required for swapping and popping the elements in removeERC20(). Additionally, the address of the ERC20 can be hardcoded to simplify the process, instead of the validator passing it.
In piNFTMethods.sol, since anyone can add the validator for any NFT collection , there's a possibility that collection addresses that are not supported can be also used.In piMarket.sol, There’s similar possibility of using the NFTs from non supported collections for direct sale and auction. We recommend the project team to decide the appropriate case according to business logic and decide whether or not to allow any NFT collection address. In case only specific addresses/implementations are allowed. The functionality can be restricted to certain addresses.
In piNFTMethods, the added validator can use any ERC20 token while adding ERC20 using addERC20(). Depending on the ERC20 token added this can be the ERC20 token with no value in the real market or the different ERC20 token than what the NFT owner is expecting.In NFTLendingBorrowing , The lender/bidder can specify the ERC20 token address that they are willing to transfer while bidding. And this is the ERC20 token that gets transferred while accepting the bid without checking if this is the same token that the NFT lister/borrower is willing to borrow. In piMarket, The seller is allowed to include any ERC20 token as _currency for their NFT sale. This functionality still varies from what is discussed with the team (i.e to allow only specific token e.g USDC to minimize the error).In FundingPool.sol, Lenders can use any ERC20 token address while lending the amount with supplyToPool().
Users can withdraw the validator’s added ERC20 tokens using withdraw() , on the withdrawal of ERC20 tokens the msg.sender needs to transfer his NFT to the contract address. Which will be transferred back to him when he will repay the withdrawn ERC20 amount.It may happen that the user decides to not repay the validator’s withdrawn ERC20 amount (and the user never receives the NFT back). This scenario is unpredictable and factors like token values may affect it, But the audit team decided to highlight these scenarios for better assessment of business logic which in turn shares more information to the project team and its users.
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (9)
350 projects reviewed · 459 reports · latest Apr 2025
See QuillAudits’s auditsSomething wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.
Tell us