mirror of
https://github.com/autistic-symposium/web3-starter-sol.git
synced 2025-07-31 18:58:41 -04:00
167 lines
5 KiB
Solidity
167 lines
5 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.17;
|
|
|
|
/*
|
|
|
|
Bi-directional payment channels allow participants Alice and Bob to repeatedly transfer Ether off chain.
|
|
|
|
Payments can go both ways, Alice pays Bob and Bob pays Alice.
|
|
|
|
Opening a channel
|
|
1. Alice and Bob fund a multi-sig wallet
|
|
2. Precompute payment channel address
|
|
3. Alice and Bob exchanges signatures of initial balances
|
|
4. Alice and Bob creates a transaction that can deploy a payment channel from
|
|
the multi-sig wallet
|
|
|
|
Update channel balances
|
|
1. Repeat steps 1 - 3 from opening a channel
|
|
2. From multi-sig wallet create a transaction that will
|
|
- delete the transaction that would have deployed the old payment channel
|
|
- and then create a transaction that can deploy a payment channel with the
|
|
new balances
|
|
|
|
Closing a channel when Alice and Bob agree on the final balance
|
|
1. From multi-sig wallet create a transaction that will
|
|
- send payments to Alice and Bob
|
|
- and then delete the transaction that would have created the payment channel
|
|
|
|
Closing a channel when Alice and Bob do not agree on the final balances
|
|
1. Deploy payment channel from multi-sig
|
|
2. call challengeExit() to start the process of closing a channel
|
|
3. Alice and Bob can withdraw funds once the channel is expired
|
|
*/
|
|
|
|
import "github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.5/contracts/utils/cryptography/ECDSA.sol";
|
|
|
|
contract BiDirectionalPaymentChannel {
|
|
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;
|
|
|
|
modifier checkBalances(uint[2] memory _balances) {
|
|
require(
|
|
address(this).balance >= _balances[0] + _balances[1],
|
|
"balance of contract must be >= to the total balance of users"
|
|
);
|
|
_;
|
|
}
|
|
|
|
// NOTE: deposit from multi-sig wallet
|
|
constructor(
|
|
address payable[2] memory _users,
|
|
uint[2] memory _balances,
|
|
uint _expiresAt,
|
|
uint _challengePeriod
|
|
) payable checkBalances(_balances) {
|
|
require(_expiresAt > block.timestamp, "Expiration must be > now");
|
|
require(_challengePeriod > 0, "Challenge period must be > 0");
|
|
|
|
for (uint i = 0; i < _users.length; i++) {
|
|
address payable user = _users[i];
|
|
|
|
require(!isUser[user], "user must be unique");
|
|
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++) {
|
|
/*
|
|
NOTE: sign with address of this contract to protect
|
|
agains replay attack on other contracts
|
|
*/
|
|
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
|
|
) {
|
|
// Note: copy storage array to memory
|
|
address[2] memory signers;
|
|
for (uint i = 0; i < users.length; i++) {
|
|
signers[i] = users[i];
|
|
}
|
|
|
|
require(
|
|
verify(_signatures, address(this), signers, _balances, _nonce),
|
|
"Invalid signature"
|
|
);
|
|
|
|
_;
|
|
}
|
|
|
|
modifier onlyUser() {
|
|
require(isUser[msg.sender], "Not user");
|
|
_;
|
|
}
|
|
|
|
function challengeExit(
|
|
uint[2] memory _balances,
|
|
uint _nonce,
|
|
bytes[2] memory _signatures
|
|
)
|
|
public
|
|
onlyUser
|
|
checkSignatures(_signatures, _balances, _nonce)
|
|
checkBalances(_balances)
|
|
{
|
|
require(block.timestamp < expiresAt, "Expired challenge period");
|
|
require(_nonce > nonce, "Nonce must be greater than the current nonce");
|
|
|
|
for (uint i = 0; i < _balances.length; i++) {
|
|
balances[users[i]] = _balances[i];
|
|
}
|
|
|
|
nonce = _nonce;
|
|
expiresAt = block.timestamp + challengePeriod;
|
|
|
|
emit ChallengeExit(msg.sender, nonce);
|
|
}
|
|
|
|
function withdraw() public onlyUser {
|
|
require(block.timestamp >= expiresAt, "Challenge period has not expired yet");
|
|
|
|
uint amount = balances[msg.sender];
|
|
balances[msg.sender] = 0;
|
|
|
|
(bool sent, ) = msg.sender.call{value: amount}("");
|
|
require(sent, "Failed to send Ether");
|
|
|
|
emit Withdraw(msg.sender, amount);
|
|
}
|
|
}
|