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