Criar um Canal de pagamento bidirecional

porMatheusem11/06/2022

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

  1. Alice e Bob financiam uma carteira multi-sig (assinatura múltipla)
  2. Pré-computação do endereço do canal de pagamento
  3. Alice e Bob trocam assinaturas de saldos iniciais
  4. Alice e Bob criam uma transação que pode implantar um canal de pagamento da carteira multi-sig

Atualizar saldo dos canais

  1. Repita as etapas 1 a 3 da abertura de um canal
  2. 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

  1. 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

  1. Implante o canal de pagamento do multi-sig
  2. chame challengeExit() para iniciar o processo de fechamento de um canal
  3. 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: MIT
pragma 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 contrato
modifier 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-sig
constructor(
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 para
proteger 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ória
address[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
)
public
onlyUser
checkSignatures(_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);
}
}

Testar no Remix