Nesse artigo iremos aprender a como criar um canal de pagamento bidirecional através de um contrato inteligente.
Canal de pagamento bidirecional
Os canais de pagamento bidirecionais permitem que os participantes Alice
e Bob
transfiram repetidamente Ether para fora da cadeia.
Os pagamentos podem ser feitos nos dois sentidos, Alice
paga Bob
e Bob
paga Alice
.
Veja como este contrato é usado:
Abertura de um canal
- Alice e Bob financiam uma carteira multi-sig (assinatura múltipla)
- Pré-computação do endereço do canal de pagamento
- Alice e Bob trocam assinaturas de saldos iniciais
- Alice e Bob criam uma transação que pode implantar um canal de pagamento da carteira multi-sig
Atualizar saldo dos canais
- Repita as etapas 1 a 3 da
abertura de um canal
- Na carteira multi-sig, crie uma transação que
- exclua a transação que implantou o antigo canal de pagamento
- e, em seguida, cria uma transação que possa implantar um canal de pagamento com os novos saldos
Fechando um canal quando Alice e Bob concordam com o saldo final
- Na carteira multi-sig, crie uma transação que
- envia pagamentos para Alice e Bob
- e, em seguida, exclua a transação que teria criado o canal de pagamento
Fechar um canal quando Alice e Bob não concordam com os saldos finais
- Implante o canal de pagamento do multi-sig
- chame challengeExit() para iniciar o processo de fechamento de um canal
- Alice e Bob podem sacar fundos assim que o canal expirar
Isso é chamado de canal de pagamento bidirecional, pois o pagamento pode ir para ambas as direções, de Alice
para Bob
ou Bob
para Alice
.
// SPDX-License-Identifier: MITpragma solidity ^0.8.13;pragma experimental ABIEncoderV2;import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/math/SafeMath.sol";import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";contract BiDirectionalPaymentChannel {using SafeMath for uint;using ECDSA for bytes32;event ChallengeExit(address indexed sender, uint nonce);event Withdraw(address indexed to, uint amount);address payable[2] public users;mapping(address => bool) public isUser;mapping(address => uint) public balances;uint public challengePeriod;uint public expiresAt;uint public nonce;// Checa na memória se os dois saldos são maiores do que// o saldo do endereço do contratomodifier checkBalances(uint[2] memory _balances) {require(address(this).balance >= _balances[0].add(_balances[1]),"saldo do contrato deve ser >= ao saldo total de usuários");_;}// NOTA: depósito da carteira multi-sigconstructor(address payable[2] memory _users,uint[2] memory _balances,uint _expiresAt,uint _challengePeriod) payable checkBalances(_balances) {require(_expiresAt > block.timestamp, "A expiração deve ser > agora");require(_challengePeriod > 0, "O período do desafio deve ser > 0");for (uint i = 0; i < _users.length; i++) {address payable user = _users[i];require(!isUser[user], "o usuário deve ser único");users[i] = user;isUser[user] = true;balances[user] = _balances[i];}expiresAt = _expiresAt;challengePeriod = _challengePeriod;}function verify(bytes[2] memory _signatures,address _contract,address[2] memory _signers,uint[2] memory _balances,uint _nonce) public pure returns (bool) {for (uint i = 0; i < _signatures.length; i++) {/*NOTA: assine com o endereço deste contrato paraproteger contra ataques de repetição de outros contratos*/bool valid = _signers[i] ==keccak256(abi.encodePacked(_contract, _balances, _nonce)).toEthSignedMessageHash().recover(_signatures[i]);if (!valid) {return false;}}return true;}modifier checkSignatures(bytes[2] memory _signatures,uint[2] memory _balances,uint _nonce) {// NOTA: copia a matriz de armazenamento para a memóriaaddress[2] memory signers;for (uint i = 0; i < users.length; i++) {signers[i] = users[i];}require(verify(_signatures, address(this), signers, _balances, _nonce),"Assinatura inválida");_;}modifier onlyUser() {require(isUser[msg.sender], "Não é usuário");_;}function challengeExit(uint[2] memory _balances,uint _nonce,bytes[2] memory _signatures)publiconlyUsercheckSignatures(_signatures, _balances, _nonce)checkBalances(_balances){require(block.timestamp < expiresAt, "Período de desafio expirado");require(_nonce > nonce, "Nonce deve ser maior que o nonce atual");for (uint i = 0; i < _balances.length; i++) {balances[users[i]] = _balances[i];}nonce = _nonce;expiresAt = block.timestamp.add(challengePeriod);emit ChallengeExit(msg.sender, nonce);}function withdraw() public onlyUser {require(block.timestamp >= expiresAt, "O período do desafio ainda não expirou");uint amount = balances[msg.sender];balances[msg.sender] = 0;(bool sent, ) = msg.sender.call{value: amount}("");require(sent, "Falha ao enviar Ether");emit Withdraw(msg.sender, amount);}}