Topshelf - Smart Contract Audit Report

Summary

crypto_audit Topshelf is a new non-custodial immutable governance free lending platform.

For this audit, we reviewed the contracts folder at commit 0ddbdc75990a9bc06742e58c6aceae68cee8f6e5 on the team's GitHub.

Audit Findings Summary
  • No external threats were identified.
  • Ensure trust in the team as they have significant control within the ecosystem.
  • Date: December 28th, 2021.
Notes on the Contracts:
BorrowerOperations Contract:
  • This contract allows users to borrow LUSD by depositing a specified token to be used as collateral. This can be done by opening a "Trove", which tracks their collateral and debt.
  • The ratio of the value of collateral tokens to LUSD borrowed for a specific Trove is called its Individual Collateral Ratio (ICR).
  • In order to open a Trove in "Normal" mode, a user must provide enough collateral to reach an ICR of at least 110%.
  • In the event that the Total Collateral Ratio of the entire platform (TCR) drops below 150%, the platform will enter "Recovery" mode.
  • In order to open a Trove in Recovery mode, a user must provide enough collateral to reach an ICR of at least 150%
  • In addition to the ICR requirement, a borrowing fee and specified constant gas compensation fee are taken upon opening a Trove.
  • The borrowing fee is capped at 5% and decays over time, resetting any time a borrow occurs.
  • There is no borrowing fee when the platform is in Recovery Mode.
  • Users may also adjust their Troves by depositing or withdrawing collateral tokens or LUSD.
  • Users are encouraged to ideally keep their ICR over 150% to reduce the risk of liquidation in the event that the platform enters Recovery Mode.
  • Users can withdraw any excess collateral or borrow more LUSD with the excess collateral unless the platform is in Recovery Mode.
  • When borrowing funds, users can specify a maximum fee percentage they are willing to pay. This helps users to avoid paying a higher fee than intended if the borrowing fee is reset before their borrow is executed.
  • Borrows must result in a user exceeding an owner-specified minimum net debt.
  • Users can close their trove and withdraw all collateral if they can pay their entire LUSD debt minus the gas compensation fee.
  • In the event where a user's Trove has been fully redeemed against, or if it has been liquidated in Recovery mode while maintaining an ICR greater than 110% (see TroveManager contract), a user can withdraw any remaining collateral from their Trove.
  • The owner can update the minimum net debt amount to any amount greater than 0 at any time.
TroveManager Contract:
  • The TroveManager contract allows users to liquidate Troves and redeem LUSD for a specified collateral token.
  • A user can liquidate any other user's Trove based on the Trove's ICR and the state of the system.
  • The liquidator will receive 0.5% of the Trove's collateral + gas compensation as a reward for liquidating. This is intended to cover gas fees and to incentivize the liquidation of larger Troves.
  • Depending on the gas compensation constant and potential gas price increases, it may be possible for a scenario to occur where liquidation is no longer profitable for the liquidator. In this case smaller troves may not be liquidated due to lack of incentive.
  • In Normal mode, a user's Trove cannot be liquidated if they have an ICR greater than or equal to 110%.
  • When a Trove is liquidated in Normal mode, the Stability Pool will take the debt. The remaining collateral (after liquidator rewards) is distributed to Stability Pool providers.
  • In the event that the Stability Pool is empty upon liquidation and thus cannot absorb debt, both the remaining collateral and debt are distributed across all active Troves. This can result in the ICR of a user's Trove to decrease.
  • In Recovery mode, a user's Trove can be liquidated if it has an ICR less than 150%. In this mode, the effects of liquidations vary based on the state of the Stability Pool and the Troves ICR.
  • When a Trove with an ICR less than 100% is liquidated in Recovery mode, both the remaining collateral and debt are distributed across active Troves. This prevents Stability Pool providers from taking a loss.
  • When a Trove with an ICR between 100% and 110% is liquidated in Recovery mode, the Stability Pool will take the debt and the remaining collateral is distributed to Stability Pool providers.
  • In the event that the Stability Pool does not have enough funds to take the debt, the Stability Pool will take whatever debt possible, if any, and the equivalent ratio of collateral will be distributed to Stability Pool providers; the remaining debt and collateral will be distributed to all active Troves.
  • When a Trove with an ICR greater than or equal to 110% (and less than 150%) is liquidated in Recovery mode, the Stability Pool will take the debt and the equivalent value of 110% of the debt in collateral tokens is distributed to Stability Pool providers. The remaining collateral can be withdrawn by the depositor through the CollSurplusPool contract so they do not lose more than their original 10% collateral fee.
  • In the event that the Stability Pool does not have enough funds to take the debt, the Trove will be ineligible for liquidation.
  • In Recovery mode, borrowing fees are also set to 0%, which are separate from the collateral needed to borrow funds.

  • When using this contract to redeem LUSD for collateral tokens, a maximum number of Troves to redeem from can be provided in order to avoid exceeding the gas limit.
  • For more gas savings, "hints" can be specified to help the platform find the correct Trove index to begin redemption from. Additional hints can be passed to specify where a Trove should be reinserted if it has been partially redeemed, as its ICR will be modified.
  • When redeeming collateral from Troves, the user pays a redemption fee == (base rate + 0.5%) * total collateral redeemed.
  • The redemption fee is capped at 100%, decaying over time. Any time a redemption occurs, the base rate is reset.
  • Any Troves fully redeemed will have their debt cancelled and will be subsequently closed.
  • After deployment, the contract will not function until the "system deployment" has been triggered. The system deployment sets the required contract address references to specified values.
  • Users cannot redeem LUSD until two weeks from the system deployment time have passed.
  • Ownership is renounced upon system deployment.
StabilityPool Contract:
  • The Stability Pool holds LUSD users elect to deposit. The LUSD is used to liquidate Troves whose collateral has fallen below the minimum ICR.
  • When the Stability Pool liquidates a Trove, each user receives a share of the collateral relative to their proportion of total LUSD deposited in the Pool.
  • As Troves are typically over-collateralized, users should experience a net gain in value from liquidation.
  • In certain situations, it is possible for a user to experience a loss in the case that a Trove is liquidated when under-collateralized.
  • During each liquidation, every user's deposited LUSD is decreased by the same percentage as the total LUSD within the pool is decreased. A liquidation that decreased the total supply of LUSD within the pool by 10% will decrease each user's deposited amount by 10%.
  • Once a user's deposited LUSD reaches 0, they will no longer receive collateral from liquidated Troves.
  • Users will receive their earned collateral when depositing and withdrawing.
  • Users also have the option to withdraw their earned collateral to a Trove, allowing them to further leverage their position.
  • In addition to the collateral gains from liquidations, users will also be distributed LIQR token rewards over time.
  • LIQR tokens are issued to the Stability Pool, where each user earns a percentage of the issued tokens based on the percentage of the total LUSD they have deposited.
  • If users have deposited through a front end, the front end will take a percentage of the issued LIQR tokens as a fee.
  • Ownership is renounced upon system deployment.
CommunityIssuance Contract:
  • This contract is used by the Stability Pool to calculate the distribution of LIQR tokens and issue them from the Treasury to Stability Pool providers over time.
  • LIQR tokens are distributed over time based on a specified "Issuance Factor".
  • The "Shutdown Admin" can pause the contract at any time; this prevents the issuance of LIQR tokens.
  • The Stability Pool address can be updated by itself at any time.
  • Ownership is renounced upon system deployment.
Vesting Contract:
  • Users can claim their tokens issued by the CommunityIssuance contract through this contract.
  • As tokens become available, users can claim their share based on their allocation points.
  • Users and their corresponding allocation points are specified upon deployment.
LUSDToken Contract:
  • LUSDTokens are minted in exchange for deposited collateral when creating a Trove.
  • Tokens are burned when the Trove is redeemed and the debt is repaid, or the Trove is liquidated.
  • In addition to all standard ERC-20 functionality, this contract implements the EIP-2612 standard in order to support permits which allows for gasless approvals to be made via signatures.
  • Tokens cannot be transferred to the StabilityPool, TroveManager, and BorrowerOperations contracts.
  • The Shutdown Admin, which is set upon deployment, may pause the contract, disabling all minting, at any time.
LIQRToken Contract:
  • LIQR tokens are earned as rewards for interacting with the Topshelf protocol.
  • In addition to all standard ERC-20 functionality, this contract implements the EIP-2612 standard in order to support permits which allows for gasless approvals to be made via signatures.
  • The initial total supply is minted to the deployer upon deployment.
  • Users may not explicitly burn their tokens, but any transfer to the 0x...dead address is treated as a burn, reducing the total supply.
  • The supplied AnyswapRouter address may mint any number of tokens, up to the maximum total supply.
  • The AnyswapRouter may burn any number of tokens.
  • The AnyswapRouter may update its own address once per 24 hours. Doing so will trigger a 24 hour cooldown period for minting and burning. After the 24 hour period, the AnyswapRouter address will be updated.
ActivePool Contract:
  • This contract is used to store the collateral provided when creating a Trove and tracks the subsequent debt for all active Troves.
  • The Borrower Operations, Trove Manager, and Stability Pool can increase and decrease the amount of debt on behalf of the user.
  • The Borrower Operations or Default Pool contracts are able to send collateral to the pool on behalf of the user.
  • The Borrower Operations and Trove Manager contracts are able to take collateral out of the pool.
  • Ownership is renounced upon system deployment.
CollSurplusPool Contract:
  • This contracts holds the extra collateral from fully redeeming a Trove and from Troves liquidated in recovery mode.
  • Users may claim any extra collateral from their Troves through the the BorrowerOperations Contract at any time.
  • Users may also elect to send the collateral to an alternate address rather than their own when claiming.
  • Ownership is renounced upon system deployment.
FlashLender Contract:
  • This contract allows users to take out a flash loan of supported tokens for a 0.09% fee.
  • Depending on the token specified, tokens will either be loaned from a lending pool or minted to the user.
  • At the end of the transaction, tokens transferred from a lending pool will be transferred back to the lending pool; minted tokens will be burned instead.
  • After funds have been returned or burned, the contract will transfer the fee from the user to a specified staking contract.
  • If any of these operations cannot be completed, the transaction will fail.
  • This contract should not support fee-on-transfer tokens for flash loans unless the proper fee exemptions are made.
  • The owner can update the staking contract address at any time.
  • The owner can update the supported tokens and lending sources at any time.
PriceFeed Contract:
  • Two sources for price are used to determine the current fair price of an asset.
  • Chainlink is used as the primary source of price data, with a BandOracle used as a secondary source if Chainlink fails to fetch an accurate price.
  • Both sources are considered to have failed to fetch an accurate price if there is a failed response, or more than 4 hours have passed since the last price fetch.
  • In the event that both sources are failing to return an accurate price, the contract defaults to the last good price that the contract received, from either source.
  • When returning to the use of Chainlink after a failure and the BandOracle is working, a maximum of 3% price difference between the two sources is allowed.
  • A maximum of a 50% change in price is allowed between any two sequential prices provided by Chainlink. As this is based on total value, assets with a higher price may experience greater price fluctuations.
  • As the maximum price change is only enforced for Chainlink, it is theoretically possible for a large price fluctuation between BandOracle prices to occur when Chainlink is failing.
MultiReward Contract:
  • When the contract is not paused, users are able to deposit one specified staking token, in return for rewards in potentially multiple various reward tokens.
  • Users must claim their rewards separately, they are not claimed during a deposit or withdrawal.
  • Users may also elect to exit the contract, withdrawing their total balance and claiming all rewards.
  • Rewards must be supplied from an external rewards distributor for each reward token.
  • The owner may set the staking token only once after deployment.
  • The owner may add a reward token, and the associated rewards distributor, at any time.
  • The owner may update the reward distributor associated with a token at any time.
  • The owner is able to withdraw any non-staking and non-rewards token in the contract at any time.
  • The owner may pause the contract at any time.
StakingRewardsPenalty Contract:
  • As long as the contract is not paused, any user may deposit a designated LP token to earn rewards.
  • A deposit fee is taken starting at 2% and decreasing by 0.5% every 13 weeks until reaching 0 after the first year.
  • A withdrawal fee is taken starting at 8% and reduced by 1% for each week tokens are staked.
  • When withdrawing, tokens are prioritized by the amount of time staked to minimize fees paid.
  • Half of the fees taken are sent to the Treasury address controlled by the team.
  • The remaining half is stored in the contract in periods of 8 weeks. After each period, the LP tokens are redeemed for the underlying assets.
  • The designated "burn token" of the pair is sent to the 0x...dead address. The other "want token" of the pair is sent to a MultiRewards Contract where it is distributed as rewards.
  • Users must manually claim their rewards, they are not claimed when depositing or withdrawing.
  • Alternatively, users have the option to exit the contract, withdrawing their whole balance and claiming all rewards.
  • The owner may withdraw any non-staking token from the contract at any time.
  • The owner may pause the contract at any time.
InitialLiquidityPool Contract:
  • This contract allows authorized users to deposit the blockchain's native token in return for rewards.
  • The deployer of the contract will specify a 1-day deposit period in which authorized users can deposit funds.
  • When depositing, the user must pass in a "claim proof" which is hashed with msg.sender and verified to match one of the contract's two "whitelist roots".
  • Of the two whitelist roots, one cannot be used until two hours have passed since the deposit start time.
  • A soft cap and hard cap on the deposits are set as the equivalent of 1 million and 5 million USD, respectively, in the blockchain's native token. These prices are calculated using a PriceFeed contract within 5 minutes of the deposit start time.
  • If the deposit results in the contracts balance exceeding the soft cap, the deposit period end will be set to 6 hours from that deposit time.
  • A deposit will not be accepted if it results in the contract's balance exceeding the hard cap.
  • Users cannot deposit more than a determined "contribution cap" within the first 4 hours of the deposit period.
  • If the deposit period has ended and the soft cap has not been reached, users may withdraw their deposited funds.
  • If the deposit period has ended and the soft cap has been reached, any address can trigger a liquidity add.
  • When a liquidity add is triggered, the contract's balance of the blockchain's native token along with its reward token balance are transferred to an LP token address.
  • The LP token address will then mint tokens to a specified Treasury address.
  • Once the "streaming period" begins, users can begin claiming rewards in the form of the contract's reward token.
  • Claimable rewards will vest linearly over the course of the streaming period.
  • Users can withdraw their total reward tokens early at a 33% fee if the streaming period has started.
  • The owner can "kill" the project at any time as long as liquidity has not been added; users will then be able to withdraw any deposited funds.
  • Some functions can be declared external for gas saving purposes.
HintHelpers Contract:
  • This contract returns various "hints" to help optimize the redemption and reinsertion of Troves.
  • Three different hints are given to assist redemption of Troves. Users provide a max number of iterations, with 0 indicating the max number of iterations.
  • Users should exercise caution to ensure a low enough max number of iterations so as to avoid hitting the block gas limit.
  • The first redemption hint is the address of the first Trove where the ICR is greater than or equal to the MCR.
  • The second redemption hint is the final nominal ICR of the last trove to be redeemed, in the case of a partial redemption. If the final Trove to be redeemed is a full redemption, this will be 0.
  • The final redemption hint is the maximum LUSD amount that can be redeemed from the provided desired LUSD amount. This may be less than the desired amount if the last Trove of the redemption would be under the minimum allowed debt.
  • The reinsertion hint provides an address that is probabilistically close to the correct insertion index based on the number of trials used.
  • Ownership is renounced upon system deployment.
General Notes Across All Contracts:
  • A separate set of contracts is required to be deployed by the team for each supported collateral token.
  • The team should exercise caution when adding fee-on-transfer tokens to the platform, and should not add ERC-777 tokens to the platform.
  • The team should exercise caution when adding support for collateral tokens that are susceptible to large price fluctuations.
  • All contracts implement SafeMath to prevent overflows/underflows.

Audit Results

Vulnerability CategoryNotesResult
Arbitrary Storage WriteN/APASS
Arbitrary JumpN/APASS
Centralization of ControlThe team retains control of the SystemShutdown contract allowing pausing of Contracts in bulk.
The team can update supported tokens or their lending pools in the FlashLender contract.
The team can update the minimum net debt for Troves at any time.
PASS
Delegate Call to Untrusted ContractN/APASS
Dependence on Predictable VariablesN/APASS
Deprecated OpcodesN/APASS
Ether ThiefN/APASS
ExceptionsN/APASS
External CallsN/APASS
Flash LoansN/APASS
Integer Over/UnderflowN/APASS
Logical IssuesN/APASS
Multiple SendsN/APASS
OraclesN/APASS
SuicideN/APASS
State Change External CallsN/APASS
Unbounded LoopsN/APASS
Unchecked RetvalN/APASS
User Supplied AssertionN/APASS
Critical Solidity CompilerN/APASS
Overall Contract Safety PASS

Contract Source Summary and Visualizations

Name

Address/Source Code

Visualized
(Hover-Zoom Recommended)

ActivePool

GitHub

Function Graph.   Inheritance Chart.

BorrowerOperations

GitHub

Function Graph.   Inheritance Chart.

CollSurplusPool

GitHub

Function Graph.   Inheritance Chart.

CommunityIssuance

GitHub

Function Graph.   Inheritance Chart.

DefaultPool

GitHub

Function Graph.   Inheritance Chart.

FlashLender

GitHub

Function Graph.   Inheritance Chart.

GasPool

GitHub

Function Graph.   Inheritance Chart.

HintHelpers

GitHub

Function Graph.   Inheritance Chart.

InitialLiquidityPool

GitHub

Function Graph.   Inheritance Chart.

LIQRToken

GitHub

Function Graph.   Inheritance Chart.

LQTYTreasury

GitHub

Function Graph.   Inheritance Chart.

LUSDToken

GitHub

Function Graph.   Inheritance Chart.

Migrations

GitHub

Function Graph.   Inheritance Chart.

MultiRewards

GitHub

Function Graph.   Inheritance Chart.

PriceFeed

GitHub

Function Graph.   Inheritance Chart.

SortedTroves

GitHub

Function Graph.   Inheritance Chart.

StabilityPool

GitHub

Function Graph.   Inheritance Chart.

StakingRewardsPenalty

GitHub

Function Graph.   Inheritance Chart.

SystemShutdown

GitHub

Function Graph.   Inheritance Chart.

TroveManager

GitHub

Function Graph.   Inheritance Chart.

Vesting

GitHub

Function Graph.   Inheritance Chart.