Solidity
August 1, 2023

Создаем свою АБС на блокчейне

Всем привет. Продолжаем создавать автоматизированную банковскую систему на блокчейне. В этой статье наш смарт-контракт Core был сильно изменен, так что будем разбираться по порядку.

Те кто не видел первую статью на эту тему настоятельно рекомендую просмотреть, чтоб понимать что происходит вообще тут.

Интересные моменты

Из интересного я переосмыслил концепцию. Изначально я хотел, чтоб мой вендор хранил всю информацию о всех банках, которые им бы пользовались. И соответственно вендор был бы нагружен всеми клиентами разных банков и самими банками. В процессе я понял, что это не удобно и слишком медленно. Я решил, что каждый банк будет интегрировать эту систему в свой смарт-контракт банка (получать экземпляр контракта через интерфейс и его адрес). Таким образом каждый смарт-контракт банка будет хранить всю информацию у себя внутри в безопасности. Из плюсов это анонимность каждого банка и эффективность. Из минусов, я пока не могу придумать как переводить средства из одно банка в другой одной транзакцией, потому что не могу менять балансы клиентов разных смарт-контрактов банков. Если у кого-то есть идеи, буду рад принять.

Вторя проблема, это отображение всего баланса в usd. Проблема в том, что наш клиент может иметь огромное количество токенов, у которых меняется цена токена за доллар и, чтоб отображать правильную цену аккаунта (в долларах) нам нужно обновлять баланс каждый определенный промежуток времени. Сделать это не трудно, но я пока не придумал как лучше.

Что мы сегодня реализуем:

Я реализую все оставшиеся функции ядра АБС. (почти)

Валютный учет
Безналичные расчеты
Учет и отчетность

Так же еще осталось много недочетов, которые я буду в будущем исправлять.

И как я уже сказал в функции "Безналичные расчеты" я не смог пока придумать перевод между разными банками (но это можно оставить на модуль SWIFT).

Код:

Вот новый контракт Core. Сначала я расскажу про изменения, потом уже про нововведения.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract Core {    
    address owner;    
    mapping(address=>bool) public ownersBank;    
    address public BANK;    
    constructor(){        
        owner = msg.sender;    
    }    
    struct client{        
        string name;        
        address clientOwner;        
        address bank;        
        uint clientType;        
        bool accountDebet;        
        bool accountCredit;        
        bytes32[] paymentInfo;        
        document[] documents;        
        uint balanceUsdt;        
        mapping(address=>uint) tokenBalance;        
        uint balanceCredit;    
    }    
    struct document{        
        uint typeDocument;        
        string documentInfo;        
        address document;    
    }    
    mapping(address=>mapping(uint=>bool)) initSubscribe;    
    mapping(address=>client) private clients;   
    mapping(address=>address) oracles;
    modifier onlyOwner() {        
        require(owner == msg.sender, "faild");        
        _;    
    }    
    modifier onlyOwnerBank(address sender){        
        require(ownersBank[sender] == true);        
        _;    
    }    
    function checkOwnerBank() public view returns(bool){        
        return ownersBank[msg.sender];    
    }    
    modifier checkTransfer(address sender, address to) {        
        require(clients[sender].clientOwner == sender, "not client");        
        require(clients[to].clientOwner == to, "not client");        
        require(msg.sender == BANK, "not bank");        
        require(clients[sender].accountDebet == true, "not open account");        
        require(clients[to].accountDebet == true, "not open account");        
        require(clients[to].bank == clients[sender].bank, "different bank");        
        _;    
    }    
    function transferBalance(uint amount, address token, address to, address sender) public checkTransfer(sender, to){        
        (, int256 price, , , ) = AggregatorV3Interface(oracles[token]).latestRoundData();        
        require(price > 0, "Invalid price");        
        unchecked {            
            // need decimals             
            clients[sender].balanceUsdt -= (amount / 1000000) * (uint(price) / 100);             
            clients[sender].tokenBalance[token] -= amount;            
            clients[to].balanceUsdt += (amount / 1000000) * (uint(price) / 100);            
            clients[to].tokenBalance[token] += amount;                    
        }    
    }    
    function addNeworacle(address token, address _oracle) public onlyOwner(){        
        oracles[token] = _oracle;    
    }        
    //updateBalanceInUSD()???...    
    //function transferFromBank()???...        
    function deposit(uint amount, address token, address sender) public {        
        require(BANK == msg.sender, "not bank");        
        require(clients[sender].clientOwner == sender, "not client");        
        require(clients[sender].accountDebet == true, "not open account");        
        require(IERC20(token).allowance(sender, address(this)) >= amount, "chek allowance");        
        IERC20(token).transferFrom(sender, BANK, amount);        
        (, int256 price, , , ) = AggregatorV3Interface(oracles[token]).latestRoundData();        
        require(price > 0, "Invalid price");        
        clients[sender].balanceUsdt += (amount / 1000000) * (uint(price) / 100);        
        clients[sender].tokenBalance[token] += amount;
    }    
    function withdrawal(uint amount, address token, address oracle, address sender) public{        
        require(BANK == msg.sender, "not bank");        
        require(clients[sender].clientOwner == sender, "not client");        
        require(clients[sender].accountDebet == true, "not open account");        
        require(IERC20(token).allowance(msg.sender, address(this)) >= amount, "chek allowance");        
        IERC20(token).transferFrom(msg.sender, sender, amount);        
        (, int256 price, , , ) = AggregatorV3Interface(oracle).latestRoundData();        
        require(price > 0, "Invalid price");        
        clients[sender].balanceUsdt -= (amount / 1000000) * (uint(price) / 100);        
        clients[sender].tokenBalance[token] -= amount;    
    }      
    function balanceTokenAccount(address tokenAddress, address oracle, address sender) public view returns(uint, uint){        
        require(clients[sender].clientOwner == sender, "not client");        
        uint balance = IERC20(tokenAddress).balanceOf(sender);        
        (, int256 price, , , ) = AggregatorV3Interface(oracle).latestRoundData();        
        require(price > 0, "Invalid price");           
        return (balance, balance*(uint(price)/100));    
    }    
    function createBank() public{        
        BANK = msg.sender;    
    }    
    function setOwner(address _owner) public{        
        require(msg.sender == BANK);        
        ownersBank[_owner] = true;    }
    function initializeClient(string calldata _name, address sender) public{        
        require(BANK == msg.sender, "not bank");        
        require(clients[sender].bank != msg.sender || clients[sender].clientOwner != sender, "already exist");        
        client storage newClientInfo = clients[sender];        
        newClientInfo.name = _name;        
        newClientInfo.clientOwner = sender;        
        newClientInfo.bank = msg.sender;        
        newClientInfo.accountDebet = false;        
        newClientInfo.accountCredit = false;
        uint size;                
        assembly {            
        size := extcodesize(sender)        
    }        
    if (size > 0) {            
        bytes4 expectedFunctionSignature = bytes4(keccak256("check(uint256,address)"));            
        (bool success, ) = sender.call(abi.encodeWithSelector(expectedFunctionSignature, 1, sender));            
        require(success, "faild");            
        if(success == true){                
            newClientInfo.clientType = 1;                
            newClientInfo.documents.push(document(0, "entity person", sender));            
        }         
    } else{            
        newClientInfo.clientType = 0;            
        newClientInfo.documents.push(document(0, "natural person", sender));        
    }        
    newClientInfo.paymentInfo.push(bytes32(keccak256(bytes("Create client"))));        
    }    
    function getClientInfo(address _client) public view returns(string memory, address, address, bytes32[] memory, uint, uint, uint) {        
        return (clients[_client].name, clients[_client].clientOwner, clients[_client].bank, clients[_client].paymentInfo, clients[_client].balanceUsdt, clients[_client].tokenBalance[0xdAC17F958D2ee523a2206206994597C13D831ec7], clients[_client].tokenBalance[0x514910771AF9Ca656af840dff83E8264EcF986CA]);    
    }    
    function addAccountDebet(address sender) public{          
        require(sender == clients[sender].clientOwner, "Not client");        
        initSubscribe[sender][0] = true;    
    }    
    function addAccountCredit(address sender) public{          
        require(sender == clients[sender].clientOwner);        
        initSubscribe[sender][1] = true;    
    }    
    function complitesubscribersAccount(address _client, uint typeAccount, address sender) public onlyOwnerBank(sender){        
        require(initSubscribe[_client][typeAccount] == true, "not complite");        
        require(typeAccount == 0 || typeAccount == 1, "error");        
        if(typeAccount == 0){            
            clients[_client].accountDebet = true;            
            initSubscribe[_client][typeAccount] = false;        
        }        
        if(typeAccount == 1){            
            clients[_client].accountCredit = true;            
            initSubscribe[_client][typeAccount] = false;        
        }    
    }    
    function deletAccountDebet(address sender) public{        
        require(sender == clients[sender].clientOwner);        
        require(clients[sender].accountDebet == true, "not open");        
        initSubscribe[sender][0] = true;    
    }    
    function deletAccountCredit(address sender) public{        
        require(sender == clients[sender].clientOwner);        
        require(clients[sender].accountCredit == true, "not open");        
        initSubscribe[sender][1] = true;    
    }    
    function comliteDeletAccount(address _client, uint typeAccount, address sender) public onlyOwnerBank(sender){
        require(initSubscribe[_client][typeAccount] == true, "not complite");        
        require(typeAccount == 0 || typeAccount == 1, "error");        
        if(typeAccount == 0){            
            clients[_client].accountDebet = false;            
            initSubscribe[_client][typeAccount] = false;        
        }        
        if(typeAccount == 1){                      
            clients[_client].accountCredit = false;            
            initSubscribe[_client][typeAccount] = false;        
        }    
    }       
}

В структура client я добавил пару новых полей:
адрес банка и отображение балансов в двух видах usd и в токенах

ownersBank теперь хранит в себе сотрудников банка. То есть у них будут открыты возможности на подобии подтверждения/открытия счета клиента.

Также добавил ораклы для просмотра цены токена в долларах. Добавил функционал отображения цены в долларах токенов и функцию для сохранения новых адресов ораклов для токенов.

Добавил функцию для открытия банка createBank. Теперь владелец банка должен вызвать данную функцию чтоб указать свой банк - банком в контракте core.

Добавил пару промежуточных функций для проверки, например checkOwnerBank

Добавил функционал депозита и вывода средств в банк, а также перевод внутри банка.
deposit withdrawal transferBalance - функции безналичного расчета.

Добавил модификатор для перевода, который проверяет, чтоб все данные сходились и разрешал выполнить перевод средств.

Также добавил контракт, который эти функции реализует, что то типа банка на минималках. В нем я буду проверять функционал всего вендора.

Есть еще тесты, которые я написал, для проверки работы всех функций.

Если вы загляните на гитхаб, то увидите там еще один смарт-контракт swap. Он нужен только для тестов, чтоб можно было за эфир купить токены и проверить функционал deposit transferBalance.

Из интересного: так как ораклы показывают свою цены в своих decimals, то пришлось выравнивать эти значения с токенами. Пока что реализация кривая и только для usdt, в будущем сделаю по умному.

balanceTokenAccount по идее выводит цену токенов в usd и сами токены.

Вопрос с approve. Так как мы должны дать разрешение на списание токенов, то это уже нужно реализовывать через фронтенд банку, как это делает например uniswap. Внутри контракта это сделать не возможно. Главное понимать, что approve мы даем не банку а вендору (core), так как он осуществляет перевод. Таким образом я хочу добиться безопасности. То есть я хочу чтоб вендор был как контракт доверия всех людей, чтоб они понимали что банк их не обманет.

Еще надо четко понимать, что каждый вызов функции контракта core идет от банка, то есть msg.sender всегда смарт-контракт в core.

В общем делать еще много, но основной функционал реализован, осталось подчистить код и решить пару проблем и с ядром АБС закончили.

доп код контракта недо-банка:

contract OtherContract {    
    address public coreAddress;     
    Core public coreInstance;     
    constructor(address _coreAddress) {        
        coreAddress = _coreAddress;        
        coreInstance = Core(coreAddress);    
    }    
    function checkClient(address client) public view returns(string memory,address, address, bytes32[] memory, uint, uint, uint){        
        return coreInstance.getClientInfo(client);    
    }    
    function setOwnerBank() public{        
        coreInstance.createBank();    
    }    
    function setOwnerClient(address owner_)  public{            
        coreInstance.setOwner(owner_);    
    }    
    function openAccount(address client, uint typeAccount) public{        
        coreInstance.complitesubscribersAccount(client, typeAccount, msg.sender);    
    }    
    function addAccountDebet() public{               
        coreInstance.addAccountDebet(msg.sender);    
    }    
    function newClient(string calldata _name) public{        
        coreInstance.initializeClient(_name, msg.sender);    
    }    
    function balance(address token) public view returns(uint){        
        return IERC20(token).balanceOf(msg.sender);    
    }
    function depositeToBank(uint amount, address token) public{        
        //approve frontend        
        coreInstance.deposit(amount, token, msg.sender);    
    }    
    function getWETHPriceInUSD(address oracle) public view returns (uint256) {        
        (, int256 price, , , ) = AggregatorV3Interface(oracle).latestRoundData();        
        require(price > 0, "Invalid price");        
        return uint256(price) / 100; //делим на 100 так как decimals оракла 8 а у usdt decimals 6 => 8-6=2 лишние нули    
    }     
    function transferBalanceInBank(uint amount, address token, address to) public{         
        coreInstance.transferBalance(amount, token, to, msg.sender);     
    }
    function checkAllowance(address token) public view returns(uint){        
        return IERC20(token).allowance(msg.sender, address(this));     
    }
}

Надеюсь кому-то интересна данная тема, потому что проект необычный и достаточно много функциональный, то есть очень много тем затрагивает, следовательно большой опыт знаний накапливается. Всем спасибо.

Оставляю кому интересно:

Мой тг канал
Этот проект на гитхабе