Quantstamp reviewed Lendroid Whalestreet (Lendroid Protocol version 1.0) on 18 Feb 2019: 15 findings, 0 critical; 11 marked fixed, 4 open.
Report hosted on certificate.quantstamp.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 | 0 | 0 | 6 |
| Low | 4 | 0 | 5 |
Added 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
L452, 453 In cancel_kernel, debugging code is active, rendering the function useless if deployed as it is.
The 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.
L247 _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.
L459 checks if collateralAmount - filledOrCancelledLoanAmount >= cancelledLoanTokenAmount. This should instead be loanAmountOffered - filledOrCancelledLoanAmount >= cancelledLoanTokenAmount according to the Solidity implementation found in Kernel.sol
borrow_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
L429 assert _kernel_creator == ecrecover(_k_hash,...) is commented out. Exploit Scenario: Malicious actors may pose as a creator
The following is a list of code defects that are bugs, do not adhere to best practices, affect gas use, or are deprecated/obsolete.
A 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
Added 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.
The 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
The report names a repository or files. Open the report to see its scope.
Repository and files the report names (2)
236 projects reviewed · 332 reports · latest Oct 2026
See Quantstamp’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