Decentralised Bank

Hey, after finishing the solidity courses I reused the Dex contract and added new features. I’ve also added other contracts to build a decentralised Bank. Its a bank that has some functions like swaps and token farming. It comunicates with the dex contract. I used strings instead of bytes32 for the tickets because i had some troubles with it. What do you guys think? :slight_smile:

Wallet.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../node_modules/@openzeppelin/contracts/utils/math/SafeMath.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";

interface IERC20 {
  
    function totalSupply() external view returns (uint256);

    function balanceOf(address account) external view returns (uint256);

    function transfer(address to, 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 from,
        address to,
        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 Wallet is Ownable{
    using SafeMath for uint256;


    mapping(address=>mapping(string=>uint256)) tokenBalances;
    mapping(address => uint256) ETHbalances;




    mapping(string=>Token) availableTokens;
    string[] tokenList;
    struct Token{
        string ticker;
        address tokenAddress;
    }
    //--------------------EVENTS-----------------------
    event deposited(address depositant, string ticker, uint amount);
    event withdrawal(address withdraw, string ticker , uint amount);
    event ETHdeposited(address depositant, uint amount);
    event ETHwithdrawal(address withdraw, uint amount);
    //-------------------------------------------------


    function addToken(string memory ticker, address tokenAddress) public onlyOwner{
        availableTokens[ticker]=Token(ticker, tokenAddress);
        tokenList.push(ticker);

    }
    function deposit(string memory ticker,uint256 amount) public {
        require(availableTokens[ticker].tokenAddress!=address(0), "That token is not available in this wallet");
        tokenBalances[msg.sender][ticker].add(amount);
        IERC20(availableTokens[ticker].tokenAddress).transferFrom(msg.sender, address(this), amount);
        emit deposited(msg.sender, ticker, amount);

    }
    function withdraw(string memory ticker, uint256 amount) public{
        require(tokenBalances[msg.sender][ticker]>=amount);
        tokenBalances[msg.sender][ticker].sub(amount);
        IERC20(availableTokens[ticker].tokenAddress).transfer(msg.sender, amount);
        emit withdrawal(msg.sender, ticker, amount);
    }
    function balanceOf(address _ad, string memory ticker) public view returns(uint256){
        return tokenBalances[_ad][ticker];
    }
    function ETHbalanceOf(address _ad) public view returns(uint256){
        return ETHbalances[msg.sender];
    }
    function depositETH() public payable{
        require(msg.value>0,"Wallet : 0 deposited");
        ETHbalances[msg.sender].add(msg.value);
        emit ETHdeposited(msg.sender, msg.value);

    }
    function withdrawETH(uint256 amount) public{
        require(ETHbalances[msg.sender]>amount);
        ETHbalances[msg.sender].sub(amount);
        (bool success,)= msg.sender.call{value : amount}("");
        emit ETHwithdrawal(msg.sender, amount);
       
    }
}




Tokens.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract Link is ERC20{
    constructor() ERC20("Chainlink", "LINK"){
        _mint(msg.sender, 100000);
    }    

}
contract ShibaInu is ERC20{
    constructor() ERC20("ShibaInu", "SHIB"){
        _mint(msg.sender, 100000);
    }
} 
contract Oasis is ERC20{
    constructor() ERC20("Oasis", "ROSE"){
        _mint(msg.sender, 100000);
    }
}
contract Fantom is ERC20{
    constructor() ERC20("FANTOM", "FTM"){
        _mint(msg.sender, 100000);
    }
}
contract Syscoin is ERC20{
    constructor() ERC20("Syscoin", "SYS"){
        _mint(msg.sender, 100000);
    }
}




Dex.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;

import "./Wallet.sol";

contract Dex is Wallet {

    using SafeMath for uint256;

    enum Side {
        BUY,
        SELL
    }

    struct Order {
        uint id;
        address trader;
        Side side;
        string ticker;
        uint amount;
        uint price;
        uint filled;
    }

    function getMarketPrice(string memory ticker) public view returns(uint256){
        uint first = orderBook[ticker][1][0].price;
        uint last = orderBook[ticker][1][orderBook[ticker][1].length].price;
        uint256 _marketPrice = (first.add(last)).div(2);
        return _marketPrice;

    }

    uint public nextOrderId = 0;

    mapping(string => mapping(uint => Order[])) public orderBook;
    mapping(address => mapping(address => mapping(string => uint256))) tokenAllowances;

    function getOrderBook(string  memory ticker, Side side) view public returns(Order[] memory){
        return orderBook[ticker][uint(side)];
    }

    function tokenApprove (address _spender, string memory ticker, uint amount) public {
        require(tokenBalances[msg.sender][ticker]>=amount);
        tokenAllowances[msg.sender][_spender][ticker].add(amount);

    }
    function createLimitOrder(Side side, string  memory ticker, uint amount, uint price) public{
        if(side == Side.BUY){
            require(ETHbalances[msg.sender]>= amount.mul(price));
        }
        else if(side == Side.SELL){
            require(tokenBalances[msg.sender][ticker] >= amount);
        }

        Order[] storage orders = orderBook[ticker][uint(side)];
        orders.push(
            Order(nextOrderId, msg.sender, side, ticker, amount, price, 0)
        );

        //Bubble sort
        uint i = orders.length > 0 ? orders.length - 1 : 0;
        if(side == Side.BUY){
            while(i > 0){
                if(orders[i - 1].price > orders[i].price) {
                    break;
                }
                Order memory orderToMove = orders[i - 1];
                orders[i - 1] = orders[i];
                orders[i] = orderToMove;
                i--;
            }
        }
        else if(side == Side.SELL){
            while(i > 0){
                if(orders[i - 1].price < orders[i].price) {
                    break;   
                }
                Order memory orderToMove = orders[i - 1];
                orders[i - 1] = orders[i];
                orders[i] = orderToMove;
                i--;
            }
        }

        nextOrderId++;
    }
    function withdrawAllETH() public{
        require(ETHbalances[msg.sender]>0);
        uint toPay = ETHbalances[msg.sender];
        ETHbalances[msg.sender]=0;
        (bool succes,)=msg.sender.call{value: toPay}("");
    }
    function transferToken(address _recipient, string memory ticker, uint amount) public returns(bool){
        require(tokenBalances[msg.sender][ticker]>=amount);
        tokenBalances[msg.sender][ticker].sub(amount);
        tokenBalances[_recipient][ticker].add(amount);
        return true;
        
    }

    function transferTokenFrom(address _spender, address _recipient, string memory ticker, uint amount) public returns(bool){
        require(tokenAllowances[_spender][msg.sender][ticker]>=amount);
        tokenBalances[msg.sender][ticker].sub(amount);
        tokenBalances[_recipient][ticker].add(amount);
        return true;
        
    }


    function createMarketOrder(Side side, string  memory ticker, uint amount) public{
        if(side == Side.SELL){
            require(tokenBalances[msg.sender][ticker] >= amount, "Insuffient balance");
        }
        
        uint orderBookSide;
        if(side == Side.BUY){
            orderBookSide = 1;
        }
        else{
            orderBookSide = 0;
        }
        Order[] storage orders = orderBook[ticker][orderBookSide];

        uint totalFilled = 0;

        for (uint256 i = 0; i < orders.length && totalFilled < amount; i++) {
            uint leftToFill = amount.sub(totalFilled);
            uint availableToFill = orders[i].amount.sub(orders[i].filled);
            uint filled = 0;
            if(availableToFill > leftToFill){
                filled = leftToFill; //Fill the entire market order
            }
            else{ 
                filled = availableToFill; //Fill as much as is available in order[i]
            }

            totalFilled = totalFilled.add(filled);
            orders[i].filled = orders[i].filled.add(filled);
            uint cost = filled.mul(orders[i].price);

            if(side == Side.BUY){
                //Verify that the buyer has enough ETH to cover the purchase (require)
                require(ETHbalances[msg.sender]>= cost);
                //msg.sender is the buyer
                tokenBalances[msg.sender][ticker] = tokenBalances[msg.sender][ticker].add(filled);
                ETHbalances[msg.sender]= ETHbalances[msg.sender].sub(cost);
                
                tokenBalances[orders[i].trader][ticker] = tokenBalances[orders[i].trader][ticker].sub(filled);
                ETHbalances[orders[i].trader] = ETHbalances[orders[i].trader].add(cost);
            }
            else if(side == Side.SELL){
                //Msg.sender is the seller
                tokenBalances[msg.sender][ticker] = tokenBalances[msg.sender][ticker].sub(filled);
                ETHbalances[msg.sender] = ETHbalances[msg.sender].add(cost);
                
                tokenBalances[orders[i].trader][ticker] =tokenBalances[orders[i].trader][ticker].add(filled);
                ETHbalances[orders[i].trader] = ETHbalances[orders[i].trader].sub(cost);
            }
            
        }
            //Remove 100% filled orders from the orderbook
        while(orders.length > 0 && orders[0].filled == orders[0].amount){
            //Remove the top element in the orders array by overwriting every element
            // with the next element in the order list
            for (uint256 i = 0; i < orders.length - 1; i++) {
                orders[i] = orders[i + 1];
            }
            orders.pop();
        }
        
    }

}

Swap.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";
import "../node_modules/@openzeppelin/contracts/utils/math/SafeMath.sol";

/*
*The interface of the DEX
*/
interface IDEX{
    function withdraw(string memory ticker, uint256 amount) external;
    function balanceOf(address _ad, string memory ticker) external view returns(uint256);
    function ETHbalanceOf(address _ad) external view returns(uint256);
    function depositETH() external payable;
    function withdrawETH(uint256 amount) external;
    
    struct Order {
        uint id;
        address trader;
        uint8 side;
        string ticker;
        uint amount;
        uint price;
        uint filled;
    }
 


      function getMarketPrice(string memory ticker) external view returns(uint256);

    function getOrderBook(string  memory ticker, uint8 side) view external returns(Order[] memory);
    function tokenApprove (address _spender, string memory ticker, uint amount) external;
    function createLimitOrder(uint8 side, string  memory ticker, uint amount, uint price) external;
    function withdrawAllETH() external;
    function transferToken(address _recipient, string memory ticker, uint amount) external;
    function transferTokenFrom(address _spender, address _recipient, string memory ticker, uint amount) external returns(bool);
    function createMarketOrder(uint8 side, string  memory ticker, uint amount) external;

}

contract Swap is Ownable{
    using SafeMath for uint256;

    IDEX exchange;
    constructor(address exchangeAddress) payable{
        exchange = IDEX(exchangeAddress);
    }
    mapping(string => bool) isAsset;
    string[] assets;
    mapping(string => uint256) bankAssets;
    
    function BankLiquidity() public virtual view returns(uint256){
        return address(this).balance;
    }
    
    function BankAssets() public virtual view returns(string[] memory){
        return assets;
    }
    function BankBalance(string memory ticker) public virtual view returns(uint256){
        return exchange.balanceOf(address(this),ticker);
    }
    function _Swap(string memory ticker, uint amount) public virtual  returns(bool){
        if(exchange.transferTokenFrom(msg.sender,address(this),ticker, amount) == true){
            bankAssets[ticker].add(amount);

            if(isAsset[ticker]==false){
                isAsset[ticker]=true;
                assets.push(ticker);
            }    
            uint256 marketPrice = exchange.getMarketPrice(ticker);
            uint256 _afterFeesPrice = marketPrice.mul(7);
             uint256 afterFeesPrice = _afterFeesPrice.div(10);
            uint256 toPay = amount.mul(afterFeesPrice);
            (bool success,)=msg.sender.call{value : toPay}("");
            return true;
        }
        else{
            return false;
        }


   }
   /*
   *
   */
   function receiveLiquidity() public virtual onlyOwner{
        exchange.withdrawAllETH();

   }
   /*
   *If its not urgent to get ETH , it will try to make profit from selling its assets in the exchange
   */

   function LiquidateWithBenefits(string memory tickerToSell, uint amount) public virtual onlyOwner{
        require(bankAssets[tickerToSell]>=amount, "Bank : Insufficient balance");
        //they will be sold at 102% their market price
        uint _sellPrice= (exchange.getMarketPrice(tickerToSell)).mul(51);
        uint sellPrice = _sellPrice.div(50);
        uint totalPrice = sellPrice.mul(sellPrice);
        exchange.createLimitOrder(0, tickerToSell, amount, totalPrice);
        bankAssets[tickerToSell].sub(amount);
   }
   /*
   *If the bank needs ETH urgently , it can sell its assets in the exchange at market price or even with losses
    */


    
   function LiquidateFast(uint8 mode, string memory tickerToSell, uint amount) public virtual onlyOwner{
       require(mode == 0|| mode ==1, "Bank : Must use either 0 for marketOrder or 1 for limitOrder with losses");
       if(mode ==0){
           exchange.createMarketOrder(0, tickerToSell, amount);
           bankAssets[tickerToSell].sub(amount);
       }else{
           uint _sellPrice = (exchange.getMarketPrice(tickerToSell)).mul(9);
           uint sellPrice = _sellPrice.div(10);
           uint totalPrice = sellPrice.mul(sellPrice);
           exchange.createLimitOrder(0, tickerToSell, amount, totalPrice);
           bankAssets[tickerToSell].sub(amount);


       }

   }


}

Farming.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./Swap.sol";

contract Farming is Swap {
    using SafeMath for uint256;
    mapping(address => mapping (string => FarmTokens)) farmingBalances; 
    struct FarmTokens{
        uint amount;
        uint whenStarted;
    }
    constructor(address dexAddress) Swap(dexAddress) payable{}
    
    function startFarming(string memory ticker, uint amount) virtual public returns(bool){
        require(isAsset[ticker]==true, "Farm : That option isnt available yet");
        require(bankAssets[ticker]>=amount,"Farm : That option isnt possible currently ");
        if(exchange.transferTokenFrom(msg.sender,address(this),ticker, amount) == true){
            farmingBalances[msg.sender][ticker]=FarmTokens(amount, block.timestamp);
            bankAssets[ticker].add(amount);
            return true;
            
            
        }else{
            return false;
        }


    }

    function withdrawFarmedTokens(string memory ticker) virtual public returns(bool) {
        uint _amount = farmingBalances[msg.sender][ticker].amount;
        uint _apy = _amount.mul(11);
        uint apy = _apy.div(10);
        exchange.transferToken(msg.sender, ticker, _apy); 
        return true;
       
        
    }
    function isLocked(address farmer, string memory ticker) public view returns(bool){
        if(farmingBalances[farmer][ticker].whenStarted + 90 days> block.timestamp){
            return true;
        }
        else{
            return true;
        }
    }


    
}

Bank.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./Farming.sol";
/*
    This Decentralised Bank doesnt store money. It only offers some services:
        *Swaps => With this service you can liquidate your tokens for Ethers without the need of going to markets
        *Loans => You can get tokens as a Loan and give it back in Ethers with some interests
        *Farming => You can lock your tokens in the bank so it can use them to offer loans, getting an APY for it.
    
    The reason why it works this way is because the bank is really interested in owning ETH over tokens. ETH is such a much more
    secure investment.

*/
contract Bank is Farming{
    using SafeMath for uint256;
    mapping(address => mapping(string => Loan)) hasLoan;
    struct Loan{
        uint amount;
        uint price;
        uint payed;
    }
    /*
    *Your trust level determines the loan you can get
    */
    mapping(address => uint) trustLevel;//the max trust level is 255, and it wont overflow thanks to safeMath
    /*
    *The bank will also be aware of who payed and how much does someone ow
    */
    mapping (address => bool) hasPayed;
    constructor(address dexContract) Farming(dexContract) payable{}
    /*
        *The bank owner will be able to tell the bank if it should give loans or swaps looking at the bank's financial state
    */
    bool hasEnoughAssets;
    bool hasEnoughLiquidity;

    function changeBankState(bool assets, bool liquidity) public onlyOwner {
        hasEnoughAssets = assets;
        hasEnoughLiquidity = liquidity;
    }
    /*
    *The loans are given back in ETH so the bank gets liquidity
    */

    function getLoan(string memory ticker ,uint amount) public {
        require(hasPayed[msg.sender]== true);
        require(isAsset[ticker]==true);
        require(bankAssets[ticker]>=amount);
        if(trustLevel[msg.sender]!=0){
        require(amount<=trustLevel[msg.sender].mul(500));
        }else{
            require(amount <= 400);
        }
        require(hasEnoughAssets);
        uint neededBalance = amount.mul(8);
        require (exchange.balanceOf(msg.sender,ticker) >=neededBalance);
        hasPayed[msg.sender]=false;
        uint marketPrice = exchange.getMarketPrice(ticker);
        uint _priceWithInterests  = marketPrice.mul(11);
        uint priceWithInterests = _priceWithInterests.div(10);
        uint loanPrice= priceWithInterests.mul(amount);
        hasLoan[msg.sender][ticker]=Loan(amount, loanPrice, 0);
        exchange.transferToken(msg.sender, ticker, amount);
    }
 
    function giveBackLoan(string memory ticker) public payable{
        require(hasPayed[msg.sender]==false);
        uint _currentMustPay = hasLoan[msg.sender][ticker].price.sub(hasLoan[msg.sender][ticker].payed);
        require(msg.value<=_currentMustPay, "You dont owe that much");
        hasLoan[msg.sender][ticker].payed.add(msg.value);
        if(hasLoan[msg.sender][ticker].payed == hasLoan[msg.sender][ticker].payed){
            delete hasLoan[msg.sender][ticker];
            hasPayed[msg.sender]=true;
            if(trustLevel[msg.sender]<10){
            trustLevel[msg.sender]++;
            }
        }

    }
    /*
    *You can swap tokens for ETH very fast with the swap service, the bank will get a fee
    */
    function _Swap(string memory ticker, uint amount) public virtual override returns(bool){
        require(hasEnoughLiquidity , "Swap : that is not possible now, try later");
        if(exchange.transferTokenFrom(msg.sender,address(this),ticker, amount) == true){
            bankAssets[ticker].add(amount);
           if(isAsset[ticker]==false){
                isAsset[ticker]=true;
                assets.push(ticker);
            } 
            //the bank will give you 70% of your tokens market price
            uint256 _afterFeesPrice = exchange.getMarketPrice(ticker).mul(7);
            uint256 afterFeesPrice = _afterFeesPrice.div(10);
            uint256 toPay = amount.mul(afterFeesPrice);
            (bool success,)=msg.sender.call{value : toPay}("");
            return true;
        }
        else{
            return false;
        }
    }

        function withdrawFarmedTokens(string memory ticker) virtual override public returns(bool) {
        require(hasEnoughAssets, "Farmer : not available now");
        uint _amount = farmingBalances[msg.sender][ticker].amount;
        uint _apy = _amount.mul(11);
        uint apy = _apy.div(10);
        exchange.transferToken(msg.sender, ticker, _apy);
        delete farmingBalances[msg.sender][ticker];
        return true;
       
        
    }

    

}
1 Like

Very nice job man! :muscle:

I will be testing it over the weekend, but for what im reading, it does looks good, by any chance do you have any unit test for it?

Congratulations for achieving this results!

Carlos Z

Not really, I tested it a bit in remix and realised safemath is giving problems(For example if i deposit in the dex it makes the transaction correctly but the balance doesnt get updated). Tried without safeMath and worked well. Why is it? Should I change the compiler version or something?