As of 3 Nov 2026, Lendroid Whalestreet has 2 audit reports by 1 security firm on Trustblock; the latest is by Quantstamp, dated 26 Nov 2020. Those reports list no open critical findings.
Record last updated . It holds audits and findings, contracts and incidents up to Nov 2026. Trustblock does not review code.
Newest first
| Audit | Security firm | Date | Scope | Critical findings | Report |
|---|---|---|---|---|---|
| Lendroid Whalestreet | Security firm: Quantstamp | Date: | Scope: Files named in the report | No open critical findings | Open · certificate.quantstamp.com |
| Lendroid Protocol version 1.0 | Security firm: Quantstamp | Date: | Scope: Files named in the report | No open critical findings | Open · certificate.quantstamp.com |
Statuses are the security firm’s own, from each report.
| Severity | Open | Acknowledged | Fixed |
|---|---|---|---|
| Critical | 0 | 0 | 0 |
| High | 0 | 0 | 3 |
| Medium | 0 | 1 | 9 |
| Low | 4 | 0 | 7 |
If a user stakes at epoch `X`, and claims his reward at epoch `Y` using `BasePool.unstakeAndClaim` or `BasePool.claim`, and if `Y` is higher enough than `X`, transactions will most likely throw due to running out of gas or will hit the block gas limit. As a result, users will lose all their rewards. This issue is due to the use of a loop inside `BasePool.earned` function that iterates over all previous epochs from `lastEpochRewardsClaimed` until the current epoch relative to the block timestamp. Note that `lastEpochRewardsClaimed's` default value is zero; hence, late stakers will face the same issue, since the iteration necessarily starts from epoch zero.
Open the reportTotal rewards between epochs 85 and 336 is almost doubled of what it should be. According to the specification and documentation in the code, the total rewards between epochs 85 and 336 should be 21.6M, but it is currently set to 41,850,000: ``` // UNIV2SHRIMPPool.sol function totalRewardsInEpoch(uint256 epoch) … { . . . else if (epoch > 84 && epoch <= 336) { totalRewards = 41850000 * (10 ** 18);// 21.6 M } . . . } ```
Open the reportThe users staked amount at epoch X will not allow them any reward at epoch Y if X < Y , meaning that their staked LP tokens are not accumulated when computing the rewards for each epoch.
Open the reportFollowing the `$HRIMP` distribution per epoch table as stated in the specification, the start time is set to be December 4th, 2020. However, in `BasePool.sol` (L30), `starttime` is set to `HEARTBEATSTARTTIME`, which in turn, is equal to the `1602288000` timestamp (`2020-10-10 00:00:00 (UTC UTC +00:00)`). Hence, the start time in the code is inconsistent with the spec.
Open the reportIn `totalRewardsInEpoch`, L88-92 adjusts `totalRewards` using a logic that is not currently documented and therefore could not be validated. As such, its effect is currently undetermined. As a consequence to this issue the total distributed reward in a series will be way less than the original `totalRewards` amounts since the number of epochs multiplied by the final reward value will be less than the original documented and set `totalRewards` values.
Open the reportEpoch zero reward will never be distributed since `BasePool.stake` will be allowed to execute only after `starttime` where `starttime` is equal to `HEARTBEATSTARTTIME`. In the opposite `_currentEpoch` will return zero only if the block timestamp is lower than `HEARTBEATSTARTTIME`. At any given timestamp the stated condition will not be satisfied simultaneously, meaning that when staking will be allowed the current epoch will be epoch 1.
Open the reportAdded as part of the second report. position_hash() can be called for two or more separate positions with the same arguments. This method mitigates this possibility by adding a _nonce value but the validation is insufficient. Exploit Scenario: 1. User calls fill_kernel() once with _is_creator_lender set as true 2. open_position() is called with _kernel_creator = lender 3. L280 _nonce is validated for the wrangler and the creator (lender) 4. User calls fill_kernel() again with the same arguments but with _is_creator_lender set as false 5. open_position() is called with the same arguments except for _kernel_creator = borrower 6. L280 _nonce is validated for the wrangler and the creator (borrower), allowing for the same _nonce value to pass 7. This results in the position_hash being the same for both positions 8. Anything that relies on position_hash will be unable to distinguish between multiple positions
Open the reportL452, 453 In cancel_kernel, debugging code is active, rendering the function useless if deployed as it is.
Open the reportThe same value self.kernels_filled[_kernel_hash] is being added twice, whereas the function should be taking the sum of the filled and cancelled loan amounts.
Open the reportL247 _position_duration_in_days is not taken into account when _total_interest is calculated. This doesn't match up with how loanAmountOwed is calculated in SimpleLoan.sol, the Solidity implementation that we used as a reference.
Open the reportL459 checks if collateralAmount - filledOrCancelledLoanAmount >= cancelledLoanTokenAmount. This should instead be loanAmountOffered - filledOrCancelledLoanAmount >= cancelledLoanTokenAmount according to the Solidity implementation found in Kernel.sol
Open the reportborrow_positions (L209) and lend_positions (L211) can be unexpectedly overwritten because the indices (last_borrow_position_index and last_lend_position_index) are not incremented/decremented correctly. Exploit Scenario: 1. User borrows twice by calling open_position(). Assume the resulting _position_hashs are 0x123 and 0x456 2. Both positions get recorded in self.borrow_positions[_borrower]. eg ({1 : 0x123, 2: 0x456}) 3. At this point, self.last_borrow_position_index[_borrower] == 2 4. User closes position 0x123 by calling close_position() 5. remove_position() L217 gets called, decrementing self.last_borrow_position_index[_borrower] 6. At this point, self.last_borrow_position_index[_borrower] == 1 7. User borrows again, self.borrow_positions[user_address][2] is overwritten with the new borrow position. The borrowing data for 0x456 is lost
Open the reportThe address parameters received as input parameters do not have any sanity checks. Example: in the constructor of `LPTokenWrapper.sol`, the `lpTokenAddress` could be `0x0`. If so, the underlying `lpToken` will also be `0x0`, which will lead to all operations dependent on it to fail.
Open the reportA user staking right before the end of an epoch will still be able to get the reward for the ongoing reward even if he staked for only one block.
Open the reportL429 assert _kernel_creator == ecrecover(_k_hash,...) is commented out. Exploit Scenario: Malicious actors may pose as a creator
Open the reportThe following is a list of code defects that are bugs, do not adhere to best practices, affect gas use, or are deprecated/obsolete.
Open the reportA reentrancy vulnerability is a scenario where an attacker can repeatedly call a function from itself, unexpectedly leading to potentially disastrous results. Here's a basic example representing the very attack which impacted The DAO in 2016: function withdraw_with_reentrancy(uint256 _amount) public { msg.sender.call.value(_amount)(); // ATTACKER CAN CALL AGAIN BEFORE // FUNCTION TERMINATES because `call` // allows msg.sender to execute any code using fallback function balances[msg.sender] -= _amount; // Balance only updated AFTER funds withdrawn } Exploit Scenario: 1. L331 When calling liquidate_position() the function checks that the position status is POSITION_STATUS_OPEN 2. L333 The transfer() method on an external ERC20 contract is called 3. There could be a re-entrancy exploit that calls liquidate_position() again and transfer tokens 4. L337 Position is finally updated to POSITION_STATUS_LIQUIDATED
Open the reportAdded as part of the second report Assertions have been removed with an added comment that refers to the error message "May not call non-constant function function_name within an assertion". The assertions should however be kept. In order to resolve the error message, return values should be saved in a temporary variable and used for the assertion afterwards.
Open the reportThe ERC20 standard stipulates that the transfer() function returns true on success. The Lendroid protocol makes external calls to transfer() in functions open_position() topup_position() liquidate_position() and close_position() asserting that the result is true (the transactions will fail otherwise). An adversarial implementation of the ERC20 token can consistently or occasionally return false on transfer() calls, which will make these methods fails. The Ethereum network does not provide the Lendroid protocol with any means of checking that an external token implements the ERC20 standard correctly. It is up to the users to verify the source codes of ERC20 tokens that they intend to use the Lendroid protocol with, however, the Lendroid team needs to inform the users about these limitations. Exploit Scenario: 1. User puts up token A as collateral and borrows a malicious Token B. 2. User tries to retrieve his deposit by calling close_position() and returning token B 3. Token B is programmed so that it fails for all transfers coming from user B 4. The lender waits until the position expires and liquidates the position, successfully collecting user B's deposit
Open the reportThe reports name a repository or files. Open a report to see its scope.
Is this your project? Show this record on your own website. The label states what the record holds, links to this page and updates by itself. It is free and needs no sign-in.
Automatic: the three strongest facts of this project’s own record (its last audit and two more), highlighted. They update with the record. Picking a fact turns Automatic off.
<head><script src="https://widget.trustblock.run/project-widget.min.js" defer></script></head><body><divdata-tb-project-widgetdata-tb-project-slug="lendroid-whalestreet"></div></body>
Paste the code where the label should appear. It uses no API credit and updates by itself.
Something 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