АБС на блокчейне, модуль аккредитив
Всем привет! Сегодня уже третья статья про АБС. Написал новый модуль аккредитив для банка. Появилось много необычного и интересного в проекте.
Начну с того, что я не бросал проект, просто времени было не много и появилась проблема в реализации, о которой я думал, но не знал что она придет так рано.
Трудности и изменения:
Начну как всегда с трудностей. Изначально я хотел, чтоб все модули соединялись между собой через наследование, это было бы удобно, потому что в одном контракте лежала бы вся информация и реализация, но проект оказался больше чем я думал и уже на втором модуле при тестах я получил ошибку при диполе о вылете из за нехватке газа, что говорит о том что невозможно оценить газ, а значит контракта слишком большой.
Решение простое: просто сделать каждый контракт отдельным.(каждый модуль это свой личный контракт). Пришлось перебирать весь код для решения проблемы. Так как контракт, где храниться информация о клиентах банка мне нужен в каждом модуле, следовательно нужно брать его экземпляр и в каждый модуль добавлять. Позже в коде все будет видно. Главная проблема заключается в том, что мы не можем менять состояние переменных структуры напрямую у экземпляра контракта, поэтому приходиться добавлять лишние функции и уже работать через них.
Также из за сложности кода решил пока что делать вендор для одного банка. Другими словами, такой вендор нужно отдельно деплоить для каждого нового банка. Но в будущем можно будет это исправить усложнив логику.
На самом деле в данной статье получилось так, что весь код прошлых статей абсолютно переписан, поэтому можно даже не смотреть на предыдущие статьи в этом плане.
Код:
Первый контракт 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));
}
}
Тут все просто. В конструктор я передаю адреса моих модулей, получаю их экземпляр и просто вызываю все функции в одном контракте. Таким образом получаю доступ ко всем функциям.
На этом я закончу данную статью, надеюсь дальше буду чаще выкладывать их, но все зависит от сложности и времени, потому что пока я писал данный код столкнулся с очень многими мелкими проблемками, которые приходилось обходить и иногда уходило на это много времени.