Bi-directional payment channels allow participants (Alice and Bob) to repeatedly transfer Ether off-chain.
Payments can go both ways, Alice can pay Bob and Bob can pay Alice.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "./ECDSA.sol";
/*
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
*/
contract BiDirectionalPaymentChannel {
using ECDSA for bytes32;
event ChallengeExit(address indexed sender, uint256 nonce);
event Withdraw(address indexed to, uint256 amount);
address payable[2] public users;
mapping(address => bool) public isUser;
mapping(address => uint256) public balances;
uint256 public challengePeriod;
uint256 public expiresAt;
uint256 public nonce;
modifier checkBalances(uint256[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,
uint256[2] memory _balances,
uint256 _expiresAt,
uint256 _challengePeriod
) payable checkBalances(_balances) {
require(_expiresAt > block.timestamp, "Expiration must be > now");
require(_challengePeriod > 0, "Challenge period must be > 0");
for (uint256 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,
uint256[2] memory _balances,
uint256 _nonce
) public pure returns (bool) {
for (uint256 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,
uint256[2] memory _balances,
uint256 _nonce
) {
// Note: copy storage array to memory
address[2] memory signers;
for (uint256 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(
uint256[2] memory _balances,
uint256 _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 (uint256 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"
);
uint256 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);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
// OpenZeppelin Contracts (last updated v4.5.0) (utils/cryptography/ECDSA.sol)
library ECDSA {
enum RecoverError {
NoError,
InvalidSignature,
InvalidSignatureLength,
InvalidSignatureS,
InvalidSignatureV
}
function _throwError(RecoverError error) private pure {
if (error == RecoverError.NoError) {
return; // no error: do nothing
} else if (error == RecoverError.InvalidSignature) {
revert("ECDSA: invalid signature");
} else if (error == RecoverError.InvalidSignatureLength) {
revert("ECDSA: invalid signature length");
} else if (error == RecoverError.InvalidSignatureS) {
revert("ECDSA: invalid signature 's' value");
} else if (error == RecoverError.InvalidSignatureV) {
revert("ECDSA: invalid signature 'v' value");
}
}
function tryRecover(bytes32 hash, bytes memory signature)
internal
pure
returns (address, RecoverError)
{
// Check the signature length
// - case 65: r,s,v signature (standard)
// - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098) _Available since v4.1._
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
return tryRecover(hash, v, r, s);
} else if (signature.length == 64) {
bytes32 r;
bytes32 vs;
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
assembly {
r := mload(add(signature, 0x20))
vs := mload(add(signature, 0x40))
}
return tryRecover(hash, r, vs);
} else {
return (address(0), RecoverError.InvalidSignatureLength);
}
}
function recover(bytes32 hash, bytes memory signature)
internal
pure
returns (address)
{
(address recovered, RecoverError error) = tryRecover(hash, signature);
_throwError(error);
return recovered;
}
function tryRecover(bytes32 hash, bytes32 r, bytes32 vs)
internal
pure
returns (address, RecoverError)
{
bytes32 s = vs
& bytes32(
0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
);
uint8 v = uint8((uint256(vs) >> 255) + 27);
return tryRecover(hash, v, r, s);
}
function recover(bytes32 hash, bytes32 r, bytes32 vs)
internal
pure
returns (address)
{
(address recovered, RecoverError error) = tryRecover(hash, r, vs);
_throwError(error);
return recovered;
}
function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)
internal
pure
returns (address, RecoverError)
{
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
//
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
// these malleable signatures as well.
if (
uint256(s)
> 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
) {
return (address(0), RecoverError.InvalidSignatureS);
}
if (v != 27 && v != 28) {
return (address(0), RecoverError.InvalidSignatureV);
}
// If the signature is valid (and not malleable), return the signer address
address signer = ecrecover(hash, v, r, s);
if (signer == address(0)) {
return (address(0), RecoverError.InvalidSignature);
}
return (signer, RecoverError.NoError);
}
function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)
internal
pure
returns (address)
{
(address recovered, RecoverError error) = tryRecover(hash, v, r, s);
_throwError(error);
return recovered;
}
function toEthSignedMessageHash(bytes32 hash)
internal
pure
returns (bytes32)
{
// 32 is the length in bytes of hash,
// enforced by the type signature above
return keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
);
}
}