CODE IS HERE: https://github.com/whymatter/hackTUM-quantstamp

//SPDX-License-Identifier: Unlicense pragma solidity 0.7.0;

import "./interfaces/IBank.sol"; import "./interfaces/IPriceOracle.sol"; import "./interfaces/IERC20.sol"; import "./libraries/Math.sol"; import "hardhat/console.sol";

contract Bank is IBank { address ETH = address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); address HAK; address PriceOracle;

mapping(address => Account) ethAccounts;
mapping(address => Account) hakAccounts;
mapping(address => BorrowAccount) borrowAccounts;

constructor(address _priceOracle, address _hakToken) {
    PriceOracle = _priceOracle;
    HAK = _hakToken;
}

function checkTokenType(address token) private view {
    require(token == ETH || token == HAK, "token not supported");
}

// Get account or create if doesn't exist
function getAccountMemory(address token, address account)
    private
    view
    returns (Account memory)
{
    if (token == ETH) {
        return ethAccounts[account];
    } else {
        return hakAccounts[account];
    }
}

function getAccountStorage(address token, address account)
    private
    view
    returns (Account storage)
{
    if (token == ETH) {
        return ethAccounts[account];
    } else {
        return hakAccounts[account];
    }
}

// Calculate the interest accrued on the account since the last stored calculation
function calcNewInterest(address token, address account)
    private
    view
    returns (uint256)
{
    Account memory user_account = getAccountMemory(token, account);
    if (user_account.lastInterestBlock == 0) {
        return 0;
    } else {
        uint256 num_blocks = DSMath.sub(
            block.number,
            user_account.lastInterestBlock
        );
        return (user_account.deposit * 3 * num_blocks) / 10000;
    }
}

// Updates interest calculated for an account.
// Updates the account in storage, updating all relevant fields.
function updateInterest(address token, address account) private {
    uint256 interest = calcNewInterest(token, account);
    Account storage user_account = getAccountStorage(token, account);
    user_account.interest += interest;
    user_account.lastInterestBlock = block.number;
}

function calcNewInterestOwed(address account)
    private
    view
    returns (uint256)
{
    BorrowAccount memory user_account = borrowAccounts[account];
    if (user_account.lastInterestBlock == 0) {
        return 0;
    } else {
        uint256 num_blocks = DSMath.sub(
            block.number,
            user_account.lastInterestBlock
        );
        return (user_account.amountBorrowed * 5 * num_blocks) / 10000;
    }
}

function updateInterestOwed(address account) private {
    uint256 interest = calcNewInterestOwed(account);
    BorrowAccount storage user_account = borrowAccounts[account];
    user_account.interestOwed += interest;
    user_account.lastInterestBlock = block.number;
}

/**
 * The purpose of this function is to allow end-users to deposit a given
 * token amount into their bank account.
 * @param token - the address of the token to deposit. If this address is
 *                set to 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE then
 *                the token to deposit is ETH.
 * @param amount - the amount of the given token to deposit.
 * @return - true if the deposit was successful, otherwise revert.
 */
function deposit(address token, uint256 amount)
    external
    payable
    override
    returns (bool)
{
    checkTokenType(token);
    // If currency is ETH, take the `value` attached to the message rather than `amount`
    // -> Ether is automatically transferred via `payable` keyword
    //   -> TODO investigate as potential vulnerability
    if (token == ETH) {
        amount = msg.value;
    }
    require(amount > 0);
    if (token == HAK) {
        // Check that the sender has enough HAK to cover the deposit
        require(IERC20(token).balanceOf(msg.sender) >= amount);
        // Transfer the sender's HAK to this account
        // TODO: understand why/how we get permission to make the transfer
        IERC20(token).transferFrom(msg.sender, address(this), amount);
    }

    Account storage account = getAccountStorage(token, msg.sender);
    updateInterest(token, msg.sender);
    account.deposit += amount;
    emit Deposit(msg.sender, token, amount);
    return true;
}

/**
 * The purpose of this function is to allow end-users to withdraw a given
 * token amount from their bank account. Upon withdrawal, the user must
 * automatically receive a 3% interest rate per 100 blocks on their deposit.
 * @param token - the address of the token to withdraw. If this address is
 *                set to 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE then
 *                the token to withdraw is ETH.
 * @param amount - the amount of the given token to withdraw. If this param
 *                 is set to 0, then the maximum amount available in the
 *                 caller's account should be withdrawn.
 * @return - the amount that was withdrawn plus interest upon success,
 *           otherwise revert.
 */
function withdraw(address token, uint256 amount)
    external
    override
    returns (uint256)
{
    // TODO: is this the correct logic?
    //  -> assume we can withdraw only from the deposit, and send the whole interest back
    checkTokenType(token);
    Account storage user_account = getAccountStorage(token, msg.sender);
    require(user_account.deposit > 0, "no balance");
    require(amount <= user_account.deposit, "amount exceeds balance");

    updateInterest(token, msg.sender);
    if (amount == 0) {
        amount = user_account.deposit;
    }
    user_account.deposit -= amount;
    uint256 total_return = amount + user_account.interest;
    user_account.interest = 0;

    if (token == HAK) {
        require(IERC20(token).balanceOf(address(this)) >= total_return);
        IERC20(token).transfer(msg.sender, total_return);
    } else {
        require(
            address(this).balance >= total_return,
            "bank has not enough ETH balance"
        );
        bool sent = msg.sender.send(total_return);
        require(sent, "failed to send ETH");
    }
    emit Withdraw(msg.sender, token, total_return);
    return total_return;
}

// Note: does not update interest!
// Basically: (deposits[account] + accruedInterest[account]) * 10000 / (borrowed[account] + owedInterest[account])
function calcCollateralRatio(
    Account memory hakAccount,
    BorrowAccount memory borrowAccount
) private view returns (uint256) {
    console.log(
        "Hak deposit = %s, ETH borrowed = %s",
        hakAccount.deposit,
        borrowAccount.amountBorrowed
    );

    require(hakAccount.deposit > 0, "no collateral deposited");

    // Check for infinite collateral
    if (hakAccount.deposit > 0 && borrowAccount.amountBorrowed == 0) {
        return type(uint256).max;
    }

    // Calculate deposit and interest accrued to user in ETH
    uint256 hak_price = IPriceOracle(PriceOracle).getVirtualPrice(HAK);
    console.log("Hak price is %s", hak_price);
    uint256 hak_deposit_as_eth = hakAccount.deposit * hak_price;
    uint256 hak_interest_as_eth = hakAccount.interest * hak_price;

    return
        ((hak_deposit_as_eth + hak_interest_as_eth) * 10000) /
        (borrowAccount.amountBorrowed + borrowAccount.interestOwed) /
        10**18;
}

/**
 * The purpose of this function is to allow users to borrow funds by using their
 * deposited funds as collateral. The minimum ratio of deposited funds over
 * borrowed funds must not be less than 150%.
 * @param token - the address of the token to borrow. This address must be
 *                set to 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, otherwise
 *                the transaction must revert.
 * @param amount - the amount to borrow. If this amount is set to zero (0),
 *                 then the amount borrowed should be the maximum allowed,
 *                 while respecting the collateral ratio of 150%.
 * @return - the current collateral ratio.
 */
function borrow(address token, uint256 amount)
    external
    override
    returns (uint256)
{
    require(token == ETH, "can only borrow ETH");
    console.log("Request to borrow %s ETH", amount);

    updateInterest(ETH, msg.sender);
    updateInterest(HAK, msg.sender);
    updateInterestOwed(msg.sender);

    Account memory hak_account = getAccountMemory(HAK, msg.sender);
    BorrowAccount memory borrow_account = borrowAccounts[msg.sender];

    if (amount == 0) {
        amount =
            ((hak_account.deposit + hak_account.interest) * 10000) /
            15000;
        amount =
            amount -
            borrow_account.amountBorrowed -
            borrow_account.interestOwed;
    }

    // Calculate hypothetical collateral ratio if the money were borrowed
    borrow_account.amountBorrowed += amount;
    uint256 new_collateral = calcCollateralRatio(
        hak_account,
        borrow_account
    );
    console.log("new collateral is %d", new_collateral);
    require(
        new_collateral >= 15000,
        "borrow would exceed collateral ratio"
    );

    // Make sure we can cover the loan
    require(address(this).balance >= amount);

    // At this point, we know we can make the loan
    BorrowAccount storage stored_borrow_account = borrowAccounts[
        msg.sender
    ];
    stored_borrow_account.amountBorrowed += amount;
    stored_borrow_account.lastInterestBlock = block.number;

    bool sent = msg.sender.send(amount);
    require(sent, "failed to send ETH");
    emit Borrow(msg.sender, ETH, amount, new_collateral);
    return new_collateral;
}

/**
 * The purpose of this function is to allow users to repay their loans.
 * Loans can be repaid partially or entirely. When replaying a loan, an
 * interest payment is also required. The interest on a loan is equal to
 * 5% of the amount lent per 100 blocks. If the loan is repaid earlier,
 * or later then the interest should be proportional to the number of
 * blocks that the amount was borrowed for.
 * @param token - the address of the token to repay. If this address is
 *                set to 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE then
 *                the token is ETH.
 * @param amount - the amount to repay including the interest.
 * @return - the amount still left to pay for this loan, excluding interest.
 */
function repay(address token, uint256 amount)
    external
    payable
    override
    returns (uint256)
{
    require(token == ETH, "token not supported");
    require(msg.value >= amount, "msg.value < amount to repay");

    BorrowAccount storage borrow_account = borrowAccounts[msg.sender];
    require(borrow_account.amountBorrowed > 0, "nothing to repay");

    updateInterestOwed(msg.sender);
    return 0;
}

/**
 * The purpose of this function is to allow so called keepers to collect bad
 * debt, that is in case the collateral ratio goes below 150% for any loan.
 * @param token - the address of the token used as collateral for the loan.
 * @param account - the account that took out the loan that is now undercollateralized.
 * @return - true if the liquidation was successful, otherwise revert.
 */
function liquidate(address token, address account)
    external
    payable
    override
    returns (bool)
{}

/**
 * The purpose of this function is to return the collateral ratio for any account.
 * The collateral ratio is computed as the value deposited divided by the value
 * borrowed. However, if no value is borrowed then the function should return
 * uint256 MAX_INT = type(uint256).max
 * @param token - the address of the deposited token used a collateral for the loan.
 * @param account - the account that took out the loan.
 * @return - the value of the collateral ratio with 2 percentage decimals, e.g. 1% = 100.
 *           If the account has no deposits for the given token then return zero (0).
 *           If the account has deposited token, but has not borrowed anything then
 *           return MAX_INT.
 */
function getCollateralRatio(address token, address account)
    public
    view
    override
    returns (uint256)
{
    checkTokenType(token);
    if (token == HAK) {
        Account memory hak_account = getAccountMemory(HAK, account);
        BorrowAccount memory borrow_account = borrowAccounts[account];

        // TODO: double-check
        hak_account.interest += calcNewInterest(HAK, account);
        borrow_account.interestOwed += calcNewInterestOwed(account);

        uint256 collateral = calcCollateralRatio(
            hak_account,
            borrow_account
        );
        console.log("Calculated collateral = %s", collateral);
        return collateral;
    }
    // TODO: WHAT TO RETURN FOR ETH?
    return 0;
}

/**
 * The purpose of this function is to return the balance that the caller
 * has in their own account for the given token (including interest).
 * @param token - the address of the token for which the balance is computed.
 * @return - the value of the caller's balance with interest, excluding debts.
 */
function getBalance(address token) public view override returns (uint256) {
    Account memory user_account = getAccountMemory(token, msg.sender);
    return
        user_account.deposit +
        user_account.interest +
        calcNewInterest(token, msg.sender);
}

}

Built With

Share this project:

Updates