DeFi Hodl Token - Smart Contract Audit Report

Summary

DeFi Hodl is a new project on the Ethereum blockchain.

We audited DeFi Hodl's contracts at commit f352111ffe047f7ca8467e8590864410f358416c on Github. Once deployed, we will verify the code and add the mainnet addresses here.

  • The token contract is deployed at 0x38d7B7123763E28803e59Eecbe338aCbd7c7F07A on mainnet.
  • The lock contract is deployed at TBD on mainnet.

  • Notes on the Token Contract:
  • The total supply of the token is 10 million tokens, all of which will be provided to the team upon deployment.
  • No minting or burning functions are present; and there is no fee on transfers.
  • No ownership-restricted functions exist
  • The contract uses compiler version ^0.8.0 which has built-in overflow protection, replacing the need for SafeMath.

  • Notes on the Lock Contract:
  • This contract allows users to lock tokens either for a duration set by the user or until the value of the token reaches a price set by the user.
  • Users can bypass the lock by calling emergencyUnlock, which charges a seperate fee set by the team, but sends all of the locked tokens (minus the fee) back to the user.
  • A fee is charged by the team upon both locking and unlocking tokens; and emergencyUnlocks as well.
  • If a user has a number of DeFi Hodl tokens set by the team, they will be exempt from these fees.
  • The contract looks to Uniswap's getAmountsOut() function to determine the value of the token. This implementation is vulnerable to price manipulation via flash loan, potentially allowing a user to extract tokens from their deposit earlier than what should be allowed. Assuming there is not an extremely large amount of value locked by a single user, and the emergencyUnlock fees are reasonable, this attack vector is unlikely to be exploited. Therefore, the team has elected not to fix this issue.
  • The team can update the fee at any time up to a hard-coded limit of 1.5%.
  • The contract uses compiler version ^0.8.0 which has built-in overflow protection, replacing the need for SafeMath.


  • Audit Findings Summary:
  • No security issues from external attackers were identified in the token contract.
  • The price-lookup mechanism employed in the Lock contract can be manipulated by an attacker.
  • As with any presale, ensure trust prior to sending ETH to the team.
  • Date: February 23rd, 2021
  • Update Date: February 23rd, 2021 - Implemented some recommendations from audit.
  • Vulnerability CategoryNotesResult
    Arbitrary Storage WriteN/APASS
    Arbitrary JumpN/APASS
    Delegate Call to Untrusted ContractN/APASS
    Dependence on Predictable VariablesN/APASS
    Deprecated OpcodesN/APASS
    Ether ThiefN/APASS
    ExceptionsN/APASS
    External CallsN/APASS
    Flash LoansThe Lock contract uses an insecure Oracle implementation.WARNING
    Integer Over/UnderflowN/APASS
    Multiple SendsN/APASS
    SuicideN/APASS
    State Change External CallsN/APass
    Unchecked RetvalN/APASS
    User Supplied AssertionN/APASS
    Critical Solidity CompilerN/APASS
    Overall Contract Safety WARNING


    Token Contract


    ERC20 Token Graph

    Multi-file Token

    
     ($) = payable function
     # = non-constant function
     
     Int = Internal
     Ext = External
     Pub = Public
     
     +  Context 
        - [Int] _msgSender
        - [Int] _msgData
    
     + [Int] IERC20 
        - [Ext] totalSupply
        - [Ext] balanceOf
        - [Ext] transfer #
        - [Ext] allowance
        - [Ext] approve #
        - [Ext] transferFrom #
    
     +  ERC20 (Context, IERC20)
        - [Pub]  #
        - [Pub] name
        - [Pub] symbol
        - [Pub] decimals
        - [Pub] totalSupply
        - [Pub] balanceOf
        - [Pub] transfer #
        - [Pub] allowance
        - [Pub] approve #
        - [Pub] transferFrom #
        - [Pub] increaseAllowance #
        - [Pub] decreaseAllowance #
        - [Int] _transfer #
        - [Int] _approve #
    							

    Click here to download the source code as a .sol file.

    
    // SPDX-License-Identifier: MIT
    
    pragma solidity ^0.8.0;
    
    abstract contract Context {
        function _msgSender() internal view virtual returns (address) {
            return msg.sender;
        }
    
        function _msgData() internal view virtual returns (bytes calldata) {
            this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691
            return msg.data;
        }
    }
    
    interface IERC20 {
        function totalSupply() external view returns (uint256);
    
        function balanceOf(address account) external view returns (uint256);
    
        function transfer(address recipient, uint256 amount) external returns (bool);
    
        function allowance(address owner, address spender) external view returns (uint256);
    
        function approve(address spender, uint256 amount) external returns (bool);
    
        function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    
        event Transfer(address indexed from, address indexed to, uint256 value);
    
        event Approval(address indexed owner, address indexed spender, uint256 value);
    }
    
    contract ERC20 is Context, IERC20 {
        mapping (address => uint256) private _balances;
    
        mapping (address => mapping (address => uint256)) private _allowances;
    
        uint256 private _totalSupply = 10000000 * 10 ** 18; // 10m
    
        string private _name = "DeFi Hodl";
        string private _symbol = "iHODL";
    
        constructor () {
            _balances[_msgSender()] = _totalSupply;
        }
    
        function name() public view virtual returns (string memory) {
            return _name;
        }
    
        function symbol() public view virtual returns (string memory) {
            return _symbol;
        }
    
        function decimals() public view virtual returns (uint8) {
            return 18;
        }
    
        function totalSupply() public view virtual override returns (uint256) {
            return _totalSupply;
        }
    
        function balanceOf(address account) public view virtual override returns (uint256) {
            return _balances[account];
        }
    
        function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
            _transfer(_msgSender(), recipient, amount);
            return true;
        }
    
        function allowance(address owner, address spender) public view virtual override returns (uint256) {
            return _allowances[owner][spender];
        }
    
        function approve(address spender, uint256 amount) public virtual override returns (bool) {
            _approve(_msgSender(), spender, amount);
            return true;
        }
    
        function transferFrom(address sender, address recipient, uint256 amount) public virtual override returns (bool) {
            _transfer(sender, recipient, amount);
    
            uint256 currentAllowance = _allowances[sender][_msgSender()];
            require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
            _approve(sender, _msgSender(), currentAllowance - amount);
    
            return true;
        }
    
        function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
            _approve(_msgSender(), spender, _allowances[_msgSender()][spender] + addedValue);
            return true;
        }
    
        function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
            uint256 currentAllowance = _allowances[_msgSender()][spender];
            require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
            _approve(_msgSender(), spender, currentAllowance - subtractedValue);
    
            return true;
        }
    
        function _transfer(address sender, address recipient, uint256 amount) internal virtual {
            require(sender != address(0), "ERC20: transfer from the zero address");
            require(recipient != address(0), "ERC20: transfer to the zero address");
    
            uint256 senderBalance = _balances[sender];
            require(senderBalance >= amount, "ERC20: transfer amount exceeds balance");
            _balances[sender] = senderBalance - amount;
            _balances[recipient] += amount;
    
            emit Transfer(sender, recipient, amount);
        }
    
        function _approve(address owner, address spender, uint256 amount) internal virtual {
            require(owner != address(0), "ERC20: approve from the zero address");
            require(spender != address(0), "ERC20: approve to the zero address");
    
            _allowances[owner][spender] = amount;
            emit Approval(owner, spender, amount);
        }
    }
    
    


    Lock Contract


    ERC20 Token Graph

    Multi-file Token

    
     ($) = payable function
     # = non-constant function
     
     Int = Internal
     Ext = External
     Pub = Public
     
     + [Int] IERC20 
        - [Ext] totalSupply
        - [Ext] balanceOf
        - [Ext] transfer #
        - [Ext] allowance
        - [Ext] approve #
        - [Ext] transferFrom #
    
     + [Int] Uniswap 
        - [Ext] getAmountsOut
    
     +  Ownable 
        - [Pub]  #
        - [Pub] owner
        - [Pub] renounceOwnership #
           - modifiers: onlyOwner
        - [Pub] transferOwnership #
           - modifiers: onlyOwner
    
     +  TokenLock (Ownable)
        - [Pub]  #
        - [Pub] getLock
        - [Pub] getTokenPrice
        - [Pub] isClaimable
        - [Pub] calculateFee
        - [Pub] lock #
        - [Pub] unlock #
        - [Pub] emergencyUnlock #
        - [Pub] setEmergencyUnlockValues #
           - modifiers: onlyOwner
        - [Pub] setFee #
           - modifiers: onlyOwner
        - [Pub] setMinHodl #
           - modifiers: onlyOwner
    							

    Click here to download the source code as a .sol file.

    
    // SPDX-License-Identifier: MIT
    
    pragma solidity ^0.8.0;
    
    interface IERC20 {
        function totalSupply() external view returns (uint256);
    
        function balanceOf(address account) external view returns (uint256);
    
        function transfer(address recipient, uint256 amount) external returns (bool);
    
        function allowance(address owner, address spender) external view returns (uint256);
    
        function approve(address spender, uint256 amount) external returns (bool);
    
        function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    
        event Transfer(address indexed from, address indexed to, uint256 value);
    
        event Approval(address indexed owner, address indexed spender, uint256 value);
    }
    
    interface Uniswap {
        function getAmountsOut(uint amountIn, address[] memory path)
            external
            view
            returns (uint[] memory amounts);
    }
    
    abstract contract Ownable {
        address private _owner;
    
        event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    
        constructor () {
            address msgSender = msg.sender;
            _owner = msgSender;
            emit OwnershipTransferred(address(0), msgSender);
        }
    
        function owner() public view virtual returns (address) {
            return _owner;
        }
    
        modifier onlyOwner() {
            require(owner() == msg.sender, "Ownable: caller is not the owner");
            _;
        }
    
        function renounceOwnership() public virtual onlyOwner {
            emit OwnershipTransferred(_owner, address(0));
            _owner = address(0);
        }
    
        function transferOwnership(address newOwner) public virtual onlyOwner {
            require(newOwner != address(0), "Ownable: new owner is the zero address");
            emit OwnershipTransferred(_owner, newOwner);
            _owner = newOwner;
        }
    }
    
    contract TokenLock is Ownable {
        struct Lock {
            address user;
            address token;
            uint256 amount;
            uint256 maxUnlockTime;
            address tradeToken; 
            uint256 minUnlockPrice;
            bool claimed;
        }
        
        event LockEvent(address indexed user, address indexed token, uint256 amount, uint256 maxUnlockTime, address tradeToken, uint256 minUnlockPrice, uint256 id);
        event UnlockEvent(uint256 id);
        
        mapping(uint256 => Lock) public locks;
        
        uint256 public lockId;
        address public uniswapAddress = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D;
        address public wethAddress = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
        address public iHodlTokenAddress = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // TODO change to real token
    
        uint256 public currentFee;
        uint256 public minHodl;
        uint256 public emergencyUnlockCosts;
    
        constructor(uint256 _currentFee, uint256 _minHodl, uint256 _emergencyUnlockCosts) {
            currentFee = _currentFee;
            minHodl = _minHodl;
            emergencyUnlockCosts = _emergencyUnlockCosts;
        }
    
        function getLock(uint256 _id) public view virtual returns (Lock memory) {
            return locks[_id];
        }
        
        function getTokenPrice(address _token, uint256 _amount, address _tradeToken) public view virtual returns (uint256) {
            if (_tradeToken == wethAddress) {
                address[] memory path = new address[](2);
                path[0] = _token;
                path[1] = wethAddress;
    
                return Uniswap(uniswapAddress).getAmountsOut(_amount, path)[1];
            } else {
                address[] memory path = new address[](3);
                path[0] = _token;
                path[1] = wethAddress;
                path[2] = _tradeToken;
    
                return Uniswap(uniswapAddress).getAmountsOut(_amount, path)[2];
            }
        }
        
        function isClaimable(uint256 _lockId) public view virtual returns (bool) {
            Lock memory lock_ = locks[_lockId];
    
            if (lock_.claimed) {
                return false;
            } else if (lock_.maxUnlockTime <= block.timestamp) {
                return true;
            } else if (lock_.minUnlockPrice <= getTokenPrice(lock_.token, lock_.amount, lock_.tradeToken)) {
                return true;
            } else {
                return false;
            }
        }
    
        function calculateFee(uint256 _amount, address _account) public view virtual returns (uint256) {
            if (IERC20(iHodlTokenAddress).balanceOf(_account) >= minHodl) {
                return 0;
            } else {
                return _amount * 100 / 1000 * currentFee;
            }
        }
    
        function lock(address _token, uint256 _amount, uint256 _maxDuration, address _tradeToken, uint256 _minUnlockPrice) public virtual {
            require(IERC20(_token).transferFrom(msg.sender, address(this), _amount), "transferFrom failed");
            uint256 maxUnlockTime_ = block.timestamp + _maxDuration;
            uint256 fee_ = calculateFee(_amount, msg.sender);
            
            if (fee_ > 0) {
                require(IERC20(_token).transfer(owner(), fee_), "fee transfer failed");
                _amount = _amount - fee_;
            }
    
            Lock memory lock_ = Lock({
                user:msg.sender,
                token:_token,
                amount:_amount,
                maxUnlockTime:maxUnlockTime_,
                tradeToken:_tradeToken,
                minUnlockPrice:_minUnlockPrice,
                claimed:false
            });
            
            lockId += 1;
            locks[lockId] = lock_;
            
            emit LockEvent(msg.sender, _token, _amount, maxUnlockTime_, _tradeToken, _minUnlockPrice, lockId);
        }
        
        function unlock(uint256 _lockId) public virtual {
            Lock memory lock_ = locks[_lockId];
            uint256 amount_ = lock_.amount;
            uint256 fee_ = calculateFee(amount_, msg.sender);
            
            if (fee_ > 0) {
                require(IERC20(lock_.token).transfer(owner(), fee_), "fee transfer failed");
                amount_ = amount_ - fee_;
            }
    
            locks[_lockId].claimed = true;
            
            require(msg.sender == lock_.user, "msg.sender does not own this lock");
            require(isClaimable(_lockId), "lock not claimable");
            require(IERC20(lock_.token).transfer(lock_.user, amount_), "transfer failed");
            
            emit UnlockEvent(_lockId);
        }
    
        function emergencyUnlock(uint256 _lockId) public virtual {
            Lock memory lock_ = locks[_lockId];
    
            locks[_lockId].claimed = true;
            
            require(msg.sender == lock_.user, "msg.sender does not own this lock");
            require(IERC20(iHodlTokenAddress).transferFrom(msg.sender, owner(), emergencyUnlockCosts), "emergencyUnlock payment failed");
            require(IERC20(lock_.token).transfer(lock_.user, lock_.amount), "transfer failed");
        }
    
        function setEmergencyUnlockValues(address _address, uint256 _amount) public virtual onlyOwner {
            iHodlTokenAddress = _address;
            emergencyUnlockCosts = _amount;
        }
    
        function setFee(uint256 _fee) public virtual onlyOwner {
            currentFee = _fee; // 1 means 0.1%
        }
    
        function setMinHodl(uint256 _minHodl) public virtual onlyOwner {
            minHodl = _minHodl;
        }
    }