August 22, 2023

АБС на блокчейне, модуль аккредитив

Всем привет! Сегодня уже третья статья про АБС. Написал новый модуль аккредитив для банка. Появилось много необычного и интересного в проекте.

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

Трудности и изменения:

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

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

Также из за сложности кода решил пока что делать вендор для одного банка. Другими словами, такой вендор нужно отдельно деплоить для каждого нового банка. Но в будущем можно будет это исправить усложнив логику.

На самом деле в данной статье получилось так, что весь код прошлых статей абсолютно переписан, поэтому можно даже не смотреть на предыдущие статьи в этом плане.

Код:

Первый контракт clientInfo - содержит информацию о клиентах и основные функции по изменению состояний полей структуры клиента.

// SPDX-License-Identifier: MITpragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract ClientInfo {        
    address public BANK;    
    address owner;    
    constructor(){        
        owner = msg.sender;    
    }    
    struct client{        
        string name;        
        address clientOwner;        
        address bank;        
        uint clientType;        
        bool accountDebet;        
        bool accountCredit;        
        payment[] paymentInfo;        
        document[] documents;        
        address[] allTokens;        
        mapping(address=>uint) balanceUsdt;        
        mapping(address=>uint) tokenBalance;    
    }    
    struct document{        
        uint typeDocument;        
        string documentInfo;        
        address document;    
    }    
    struct payment{        
        address from;        
        address to;        
        string message;        
        address token;        
        uint amount;    
    }    
    mapping(address=>mapping(uint=>bool)) public initSubscribe;    
    mapping(address=>client) public clients;        
    mapping(address=>address) oracles;    
    mapping(address=>bool) public ownersBank;
    mapping(address=>bool) module;    
    modifier onlyModules(address module_) {        
        require(module[module_] == true, "only module can call");        
        _;    
    }    
    modifier onlyOwner(){        
        require(msg.sender == owner, "not owner");        
        _;    
    }    
    function addModules(address _module) public onlyOwner{        
        module[_module] = true;    
    }    
    function createBank() public onlyModules(msg.sender){        
        BANK = msg.sender;        
        client storage newClientInfo = clients[msg.sender];        
        newClientInfo.name = "BANK";        
        newClientInfo.clientOwner = msg.sender;        
        newClientInfo.bank = msg.sender;        
        newClientInfo.accountDebet = true;        
        newClientInfo.accountCredit = true;        
        newClientInfo.clientType = 3;        
        newClientInfo.documents.push(document(0, "create bank", msg.sender));    
    }
    function addNeworacle(address token, address _oracle) public onlyModules(msg.sender){        
        oracles[token] = _oracle;    
    }
    function initializeClient(string calldata _name, address sender) public onlyModules(msg.sender){        
        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(payment(sender, address(this), "new client", address(this), 0));        
    }       
    function setOwner(address _owner) public onlyModules(msg.sender){       
        require(msg.sender == BANK);        
        ownersBank[_owner] = true;    
    }
    function getClientInfo(address sender) public view onlyModules(msg.sender) returns(string memory,address, address, uint, uint, uint) {        
        return(clients[sender].name, clients[sender].clientOwner, clients[sender].bank, clients[sender].clientType, clients[sender].balanceUsdt[0xdAC17F958D2ee523a2206206994597C13D831ec7], clients[sender].tokenBalance[0xdAC17F958D2ee523a2206206994597C13D831ec7]);    
    }
    function setSubscribe(address sender, uint typeClient) public onlyModules(msg.sender){        
        initSubscribe[sender][typeClient] = true;    
    }
    function addAccount(address sender, uint typeAccount) public onlyModules(msg.sender){        
        if(typeAccount == 0){            
            clients[sender].accountDebet = true;        
        }        
        if(typeAccount == 1){            
            clients[sender].accountCredit = true;        
        }            
    }    
    function deletAccount(address sender, uint typeAccount) public onlyModules(msg.sender){         
        if(typeAccount == 0){            
            clients[sender].accountDebet = false;        
        }        
        if(typeAccount == 1){            
            clients[sender].accountCredit = false;        
        }    
    }    
    function updateBalanceInUSD(address sender) public{        
        for(uint i = 0; i < clients[sender].allTokens.length; i++){            
            (, int256 price, , , ) = AggregatorV3Interface(oracles[clients[sender].allTokens[i]]).latestRoundData();            
            uint balance = clients[sender].balanceUsdt[clients[sender].allTokens[i]];            
            clients[sender].balanceUsdt[clients[sender].allTokens[i]] = balance * uint(price);        
         }    
     }    
     function transferBalanceInBank(address sender, address to, address token, uint amount, string memory inf) public{        
         (, int256 price, , , ) = AggregatorV3Interface(oracles[token]).latestRoundData();        
         require(price > 0, "Invalid price");        
         unchecked {            
             clients[sender].balanceUsdt[token] -= amount * uint(price);            
             clients[sender].tokenBalance[token] -= amount;            
             clients[to].balanceUsdt[token] += amount * uint(price);            
             clients[to].tokenBalance[token] += amount;                    
         }        
         clients[sender].paymentInfo.push(payment(sender, to, inf, token, amount));    
     }    
     function invest(address sender, address token, uint amount) public{        
         (, int256 price, , , ) = AggregatorV3Interface(oracles[token]).latestRoundData();        
         require(price > 0, "Invalid price");        
         clients[sender].balanceUsdt[token] -= amount * uint(price);        
         clients[sender].tokenBalance[token] -= amount;        
         clients[BANK].balanceUsdt[token] += amount * uint(price);        
         clients[BANK].tokenBalance[token] += amount;        
         clients[sender].paymentInfo.push(payment(sender, address(this), "deposit balance", token, amount));    
     }     
     function depositBalance(address sender, address token, uint amount) public{        
         (, int256 price, , , ) = AggregatorV3Interface(oracles[token]).latestRoundData();        
         require(price > 0, "Invalid price");        
         clients[sender].balanceUsdt[token] += amount * uint(price);        
         clients[sender].tokenBalance[token] += amount;        
         clients[sender].paymentInfo.push(payment(sender, address(this), "deposit balance", token, amount));    
     }    
     function withdrawlBalance(address sender, address token, uint amount) public{        
         IERC20(token).transfer(sender, amount);        
         (, int256 price, , , ) = AggregatorV3Interface(oracles[token]).latestRoundData();        
         require(price > 0, "Invalid price");        
         unchecked{            
             clients[sender].balanceUsdt[token] -= amount * uint(price);            
             clients[sender].tokenBalance[token] -= amount;        
         }        
         clients[sender].paymentInfo.push(payment(address(this), sender, "withdrawal balance", token, amount));    
     }
}

В данном контракте все функции, которые вы уже видели, они были в контакте Core, но из за особенностей solidity они теперь тут. Единственное, что я добавил, это модификатор onlyModules на ряд функций, что их вызывать могут только модули. Это дает нам гарантию, что извне ни кто не будет обращаться к этим функциям, так как мне это не нужно. Ну и для тех полей структуры, которые мне нужно будет менять значение я сделал функцию по их изменению, например setSubscribe функция которая устанавливает в маппинге initSubscribe значение true.

Контракт CreateClients - содержит функционал создания клиента банка

// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.9;
import "./clientsInfo.sol";
contract CreateClients{    
    ClientInfo public clientInfo;    
    address letterOfCreditAddress;    
    constructor(address clientInfo_){        
        clientInfo = ClientInfo(clientInfo_);        
        letterOfCreditAddress = clientInfo_;    
    }    
    modifier onlyOwnerBank(address sender) {        
        require(clientInfo.ownersBank(sender) == true, "Not an owner bank");        
        _;    
    }    
    function checkOwnerBank() public view returns(bool){        
        return clientInfo.ownersBank(msg.sender);    
    }        
    function addAccountDebet(address sender) public {        
        (, address isOwnerAccount, , , ,) = clientInfo.clients(sender);        
        require(isOwnerAccount == sender, "Not client");        
        clientInfo.setSubscribe(sender, 0);    
    }    
    function addAccountCredit(address sender) public{          
        (, address isOwnerAccount, , , ,) = clientInfo.clients(sender);        
        require(isOwnerAccount == sender, "Not client");        
        clientInfo.setSubscribe(sender, 1);            
    }    
    function complitesubscribersAccount(address _client, uint typeAccount, address sender) public onlyOwnerBank(sender){        
        require(clientInfo.initSubscribe(_client, typeAccount) == true, "not complite");        
        require(typeAccount == 0 || typeAccount == 1, "error");        
        clientInfo.addAccount(_client, typeAccount);    
    }    
    function deletAccountDebet(address sender) public{        
        (, address isOwnerAccount, , , bool debet,) = clientInfo.clients(sender);        
        require(isOwnerAccount == sender, "Not client");        
        require(debet == true, "not open");         
        clientInfo.setSubscribe(sender, 0);    
    }    
    function deletAccountCredit(address sender) public{        
        (, address isOwnerAccount, , , , bool credit) = clientInfo.clients(sender);        
        require(isOwnerAccount == sender, "Not client");        
        require(credit == true, "not open");        
        clientInfo.setSubscribe(sender, 1);    
    }    
    function comliteDeletAccount(address _client, uint typeAccount, address sender) public onlyOwnerBank(sender){        
        require(clientInfo.initSubscribe(_client, typeAccount) == true, "not complite");        
        require(typeAccount == 0 || typeAccount == 1, "error");        
        clientInfo.deletAccount(_client, typeAccount);    
    }
}

Тут тоже все функции знакомые. Показываю, чтоб было понятно как я обращаюсь к контракту clientInfo и изменяю состояния полей структуры

Из интересного вот как вытаскивать поля из структуры, другие способы почему что не работают, компилятор ругается мол не видит их.

(, address isOwnerAccount, , , ,) = clientInfo.clients(sender);

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

 clientInfo.BANK()

Или к маппингу так:

 clientInfo.ownersBank(sender) 

то есть в случае наследования было бы так:

clientInfo.ownersBank[sender]

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

Контракт Core - тоже старый контракт, все функции в нем знакомы, но покажу как его изменил с новым подходом:

 // SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.9;
import "./Clients.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract Core is CreateClients{    
    address owner;    
    constructor(address clientInfo_) CreateClients(clientInfo_){        
        owner = msg.sender;    
    }    
    modifier onlyOwner() {        
        require(owner == msg.sender, "faild");        
        _;    
    }    
    function transferBalance(uint amount, address token, address to, address sender) public{        
        (, address isOwnerAccount, address bank, , bool debet,) = clientInfo.clients(sender);        
        (, address isOwnerAccountTo, address bankTo, , bool debetTo,) = clientInfo.clients(to);        
        require(isOwnerAccount == sender, "not client");        
        require(isOwnerAccountTo == to, "not client");        
        require(msg.sender == clientInfo.BANK(), "not bank");        
        require(debet == true, "not open account");        
        require(debetTo == true, "not open account");        
        require(bank == bankTo, "different bank");        
        clientInfo.updateBalanceInUSD(sender);        
        clientInfo.transferBalanceInBank(sender, to, token, amount, "new transfer balance");    
    }    
    function investing(address sender, address token, uint amount) public{        
        clientInfo.invest(sender, token, amount);    
    }    
    function deposit(uint amount, address token, address sender) public {        
        (, address isOwnerAccount, , , bool debet,) = clientInfo.clients(sender);        
        require(clientInfo.BANK() == msg.sender, "not bank");        
        require(isOwnerAccount == sender, "not client");        
        require(debet == true, "not open account");        
        require(IERC20(token).allowance(sender, address(this)) >= amount, "chek allowance");        
        clientInfo.updateBalanceInUSD(sender);        
        IERC20(token).transferFrom(sender, letterOfCreditAddress, amount);        
        clientInfo.depositBalance(sender, token, amount);    
    }    
    function withdrawal(uint amount, address token, address sender) public{        
        (, address isOwnerAccount, , , bool debet,) = clientInfo.clients(sender);        
        require(clientInfo.BANK() == msg.sender, "not bank");        
        require(isOwnerAccount == sender, "not client");        
        require(debet == true, "not open account");        
        require(IERC20(token).allowance(msg.sender, address(this)) >= amount, "chek allowance");        
        clientInfo.updateBalanceInUSD(sender);        clientInfo.withdrawlBalance(sender, token, amount);    
    }  
}

По факту тут идет дублирование функций из clientInfo, но с добавлением ряда условий и ограничений, сделал так для безопасности, и в рамках модификатора onlyModules. Ну и конечно наследует контракт CreateClients.

Новый контракт LettersOfCredit - это новый модуль который я реализовал в нашей АБС. Данный модуль это аккредитивы. Они работают по типу совершение сделки через гаранта, а банк в его роли. Например я (продавец) хочу продать 100 телефонов покупателю оптом, я заключаю аккредитив с банком и говорю, что после того как эти телефоны прибудут по месту назначения, банк покупателя обязуется выплатить деньги за эти телефоны в пользу продавцы, таким образом продавец уверен что его не обманут после отправки товара и ему заплатят. После покупатель уже с банком разбирается и оплачивает данный товар по частям как кредит или одним чеком. Я реализовал лишь концепцию и не соблюдал много условий, но в общем пойдет.

 // SPDX-License-Identifier: MIT
 pragma solidity ^0.8.9;
 import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 import "./clientsInfo.sol";
 contract LettersOfCredit{    
     ClientInfo clientInfo;     
     struct letterofcredit{        
     address from;        
     address to;        
     uint amount;        
     address token;        
     string conditions;        
     uint stage;    
     }    
     mapping(address=>letterofcredit) public LettersOfCredits;    
         constructor(address clientInfo_){        
         clientInfo = ClientInfo(clientInfo_);    
     }    
     event newLetterOfCreditrequest(address to, string message);     
     function makeLettersOfCredit(address sender, address recipient, uint amount, address token, string calldata contitions) public {
         letterofcredit storage newLetterOfCredit = LettersOfCredits[sender];        
         newLetterOfCredit.from = sender;        
         newLetterOfCredit.to = recipient;        
         newLetterOfCredit.amount = amount;        
         newLetterOfCredit.token = token;        
         newLetterOfCredit.conditions = contitions;        
         newLetterOfCredit.stage = 1;    
     }    
     function confirmLetterOfCredit(address sender, address _client) public {        
         require(LettersOfCredits[_client].stage == 1, "Error");        
         require(clientInfo.ownersBank(sender) == true, "only owner bank can confirm");        
         LettersOfCredits[_client].stage = 2;    
     }    
     function sendLettersOfCredit(address sender, address _client) public{       
         require(LettersOfCredits[_client].stage == 2, "Error");        
         require(clientInfo.ownersBank(sender) == true, "only owner bank can send");        
         emit newLetterOfCreditrequest(LettersOfCredits[_client].to, LettersOfCredits[_client].conditions);        
         LettersOfCredits[_client].stage = 3;    
     }    
     function checkLetterOfCredit(address sender, address _client) public{        
         require(LettersOfCredits[_client].stage == 3, "Error");        
         require(clientInfo.ownersBank(sender) == true, "only bank can confirm check");        
         LettersOfCredits[_client].stage = 4;    
     }    
     function transferBalance_(uint amount, address token, address to, address sender) private{        
         clientInfo.updateBalanceInUSD(sender);        
         clientInfo.transferBalanceInBank(sender, to, token, amount, "new transfer balance letter of credit");    
     }       
     function checkLetter(address sender) public view returns(address, address, uint, address, string memory, uint){        
         return (LettersOfCredits[sender].from, LettersOfCredits[sender].to, LettersOfCredits[sender].amount, LettersOfCredits[sender].token, LettersOfCredits[sender].conditions, LettersOfCredits[sender].stage);    
     }    
     function sendMoney(address sender, address _client) public{        
         require(LettersOfCredits[_client].stage == 4, "error");        
         require(clientInfo.ownersBank(sender) == true, "only bank can send money");        
         transferBalance_(LettersOfCredits[_client].amount, LettersOfCredits[_client].token, LettersOfCredits[_client].to, clientInfo.BANK());        
         LettersOfCredits[_client].stage = 5;    
     }    
     function compliteLetterOfCredit(address sender, address _client) public {        
         require(LettersOfCredits[_client].stage == 5, "error");        
         require(clientInfo.ownersBank(sender) == true, "only bank can send money");        
         delete LettersOfCredits[_client];    
     }
 }

Я создаю еще одну структуру, которая хранит в себе всю нужную информацию о аккредитиве. В целом все поля понятные кроме stage. Его я использую для определения этапа выполнения сделки.

Функции:

makeLettersOfCredit - функция для создание аккредитива, передаем просто все данные, после создания аккредитива мы от лица банка его подтверждаем

confirmLetterOfCredit - тут как раз вступает в игру stage. После подтверждения он меняется на 2. Как видно каждую функцию я ограничиваю stage, и ее можно вызвать только в определенный момент, а не когда замочиться.

sendLettersOfCredit - функция оповещающая о том, что аккредитив вступает в силу, вызывая событие, так как их легко отслеживать на фронте.

checkLetterOfCredit - функция вызывается после того как банк проверил что услуга выполнена и можно отправлять деньги

transferBalance_ - приватная функция, которая делает сам перевод.

checkLetter - функция которая может выдать информацию об аккредитиве

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

compliteLetterOfCredit - эта функция закрывает аккредитив после его выполнения

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

Контракт OtherContract - уже знакомый нам ипровезированный банк (надо бы название поменять).

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
import "./Core.sol";
import "./letterOfCredit.sol";
contract OtherContract{    
    Core public coreInstance;    
    LettersOfCredit public letterInsctance;    
    ClientInfo public clientInfo;    
    constructor(address _coreAddress, address _LetterOfCredit, address clientInfo_) {        
        coreInstance = Core(_coreAddress);        
        letterInsctance = LettersOfCredit(_LetterOfCredit);        
        clientInfo = ClientInfo(clientInfo_);    
    }    
    function checkClient(address client) public view returns(string memory,address, address, uint, uint, uint){        
        return clientInfo.getClientInfo(client);    
    }        
    function setOwnerBank() public{        
        clientInfo.createBank();    
    }    
    function setOwnerClient(address owner_)  public{            
        clientInfo.setOwner(owner_);    
    }    
    function addNeworacle_(address token, address oracle) public{        
        clientInfo.addNeworacle(token, oracle);    
    }    
    function investing_(address token, uint amount) public{        
        coreInstance.investing(msg.sender, token, amount);    
    }    
    function _checkLetter(address sender) public view returns(address, address, uint, address, string memory, uint){        
        return letterInsctance.checkLetter(sender);    
    }    
    function _makeLetterOfCerdit(address to, uint amount, address token, string calldata contitions) public {        
        letterInsctance.makeLettersOfCredit(msg.sender, to, amount, token, contitions);    
    }    
    function _confirmLetterOfCredit(address _client) public{        
        letterInsctance.confirmLetterOfCredit(msg.sender, _client);    
    }    
    function _sendLettersOfCredit(address _client) public{        
        letterInsctance.sendLettersOfCredit(msg.sender, _client);    
    }    
    function _checkLetterOfCredit(address _client) public{        
        letterInsctance.checkLetterOfCredit(msg.sender, _client);    
    }    
    function _sendMoney(address _client) public{        
        letterInsctance.sendMoney(msg.sender, _client);    
    }    
    function _compliteLetterOfCredit(address _client) public{        
        letterInsctance.compliteLetterOfCredit(msg.sender, _client);    
    }    
    function openAccount(address _client, uint typeAccount) public{        
        coreInstance.complitesubscribersAccount(_client, typeAccount, msg.sender);    
    }    
    function deletAccount(address _client, uint typeAccount) public{        
        coreInstance.comliteDeletAccount(_client, typeAccount, msg.sender);            
    }    
    function deletAccountDebet_() public{               
        coreInstance.deletAccountDebet(msg.sender);    
    }    
    function deletAccountCredit_() public{               
        coreInstance.deletAccountCredit(msg.sender);    
    }    
    function addAccountDebet_() public{               
        coreInstance.addAccountDebet(msg.sender);    
    }    
    function addAccountCredit_() public{               
        coreInstance.addAccountCredit(msg.sender);    
    }    
    function newClient(string calldata _name) public{        
        clientInfo.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 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));    
    }
}

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

На этом я закончу данную статью, надеюсь дальше буду чаще выкладывать их, но все зависит от сложности и времени, потому что пока я писал данный код столкнулся с очень многими мелкими проблемками, которые приходилось обходить и иногда уходило на это много времени.

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

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