Skip to content

Commit

Permalink
Merge db3064b into 42787e2
Browse files Browse the repository at this point in the history
  • Loading branch information
AugustoL committed Mar 28, 2018
2 parents 42787e2 + db3064b commit 9c42aa9
Show file tree
Hide file tree
Showing 3 changed files with 379 additions and 0 deletions.
202 changes: 202 additions & 0 deletions contracts/token/ERC20/ERC20Channel.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
pragma solidity ^0.4.18;

import "./ERC20.sol";
import "../../math/SafeMath.sol";
import "../../ownership/Ownable.sol";
import "../../ECRecovery.sol";

/**
@title ERC20Channel, State channel for ERC20 Tokens
Contract that provides holders of a ERC20 compatible token the creation of a
state channel between two users, once a channel is open the users can exchange
signatures offchain to agree on the final value of the transfer.
Uses OpenZeppelin ERC20 and SafeMath lib.
Note: The owner of the contract is the sender, therefore it should be
deployed by the sender itself.
The channel can be closed in two ways: With an agreement or not.
- Contract closed with an agreement:
Can happen at anytime while the channel is still opened. Two signatures
are needed for this, one is the signature of the receiver agreeing to
receive a certain value, with the receiver signature the sender can now
agree on the value and use sign the receiver signature.
cooperativeClose(
FinalBalance,
Receiver.sign(FinalBalance),
Sender.sign(SHA3( Receiver.sign(FinalBalance) ))
)
- Contract closed without agreement:
This can happen only in behalf of the sender, and it has a time to be
"challenged" by the receiver. The sender and receiver exchange signatures
of a final value to be transfered but they reach a point where they dont
agree on the final value. The sender can ask to close the channel with a
final value, if the challenge time passes and the channel does not receive
a cooperativeClose request it would be able to be closed by the sender
transfering the final value requested.
*/
contract ERC20Channel is Ownable {
using SafeMath for uint256;

// Add recover method for bytes32 using ECRecovery lib from OpenZeppelin
using ECRecovery for bytes32;

// The ERC20 token to be used
ERC20 public token;

// The amount of time that a receiver has to challenge the sender
uint256 public challengeTime;

// The receiver of the tokens
address public receiver;

// The closing balance requested by the sender
uint256 public closingBalance;

// The timestamp of when the channel can be closed by the sender
uint256 public closeTime;

// Event triggered when the channel close is requested
event closeRequested(uint256 closeTime, uint256 closingBalance);

// Channel closed
event channelClosed(uint256 closeTime, uint256 finalBalance);

/**
* @dev Constructor
* @param tokenAddress address, the address of the ERC20 token that will be used
* @param _receiver address, the address of the receiver
* @param _challengeTime uint256, the time that a channel has before ends
with an uncooperative close
*/
function ERC20Channel(address tokenAddress, address _receiver, uint256 _challengeTime) public {
require(tokenAddress != address(0));
require(_receiver != address(0));
require(_challengeTime > 0);

token = ERC20(tokenAddress);
challengeTime = _challengeTime;
receiver = _receiver;
}

/*
* External functions
*/

/**
* @dev Request an uncooperativeClose, it can be called only by the
sender/owner. It will save the closing balance requested and start the
challenge time period where the receiver can ask for a cooperativeClose.
* @param balance uint256, the final balance of the receiver
*/
function uncooperativeClose(uint256 balance) external onlyOwner {
// Check that the closing request doesn't exist
require(closeTime == 0);

// Check that the balance is less or equal to the token balance
require(balance <= token.balanceOf(address(this)));

// Mark channel as closed and create closing request
closeTime = now.add(challengeTime);
closingBalance = balance;
closeRequested(closeTime, closingBalance);
}

/**
* @dev Close a channel with the agreement of the sender and receiver
* @param balance uint256, the final balance transfered of the channel
* @param balanceMsgSig bytes, the signature of the sender.
The msg signed by the receiver is generateBalanceHash(balance)
* @param closingSig bytes, the signature of the receiver
The msg signed by the sender is generateKeccak256(balanceMsgSig)
*/
function cooperativeClose(uint256 balance, bytes balanceMsgSig, bytes closingSig) external {
// Derive receiver address from signature
require(receiver == keccak256(balanceMsgSig).recover(closingSig));

// Derive sender address from signed balance proof
address sender = getSignerOfBalanceHash(balance, balanceMsgSig);
require(sender == owner);

close(balance);
}

/**
* @dev Close a channel with an uncooperativeClose already requested
*/
function closeChannel() external onlyOwner {
// Check that the closing request was created
require(closeTime > 0);

// Make sure the challengeTime has ended
require(block.timestamp > closeTime);

close(closingBalance);
}

/*
* Public functions
*/

/**
* @dev Get the channel info
*/
function getInfo() public view returns (uint256, uint256, uint256) {
return (token.balanceOf(address(this)), closeTime, closingBalance);
}

/**
* @dev Get the signer of a balance hash signed
* @param balance uint256, the balance to hash
* @param msgSigned bytes, the balance hash signed
*/
function getSignerOfBalanceHash(uint256 balance, bytes msgSigned) public view returns (address) {
bytes32 msgHash = generateBalanceHash(balance);
return msgHash.recover(msgSigned);
}

/**
* @dev Generate a hash balance for an address
* @param balance uint256, the balance to hash
*/
function generateBalanceHash(uint256 balance) public view returns (bytes32) {
return keccak256(receiver, balance, address(this));
}

/**
* @dev Generate a keccak256 hash
* @param message bytes, the mesage to hash
*/
function generateKeccak256(bytes message) public pure returns(bytes32) {
return keccak256(message);
}

/*
* Internal functions
*/

/**
* @dev Close a channel
* @param finalBalance uint256, the final balance of the receiver
*/
function close(uint256 finalBalance) internal {
uint256 tokenBalance = token.balanceOf(address(this));
require(finalBalance <= tokenBalance);

// Send balance to the receiver
require(token.transfer(receiver, finalBalance));

// Send remaining balance back to sender
require(token.transfer(owner, tokenBalance.sub(finalBalance)));

// Destroy contract
selfdestruct(owner);

channelClosed(now, finalBalance);
}

}
53 changes: 53 additions & 0 deletions test/helpers/accounts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* A helper module to have access to the addresses and private keys of the
* accounts used by testrpx
*/
export const accounts = [
{
'address': '0xdf08f82de32b8d460adbe8d72043e3a7e25a3b39',
'key': '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200',
}, {
'address': '0x6704fbfcd5ef766b287262fa2281c105d57246a6',
'key': '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501201',
}, {
'address': '0x9e1ef1ec212f5dffb41d35d9e5c14054f26c6560',
'key': '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501202',
}, {
'address': '0xce42bdb34189a93c55de250e011c68faee374dd3',
'key': '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501203',
}, {
'address': '0x97a3fc5ee46852c1cf92a97b7bad42f2622267cc',
'key': '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501204',
}, {
'address': '0xb9dcbf8a52edc0c8dd9983fcc1d97b1f5d975ed7',
'key': '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501205',
}, {
'address': '0x26064a2e2b568d9a6d01b93d039d1da9cf2a58cd',
'key': '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501206',
}, {
'address': '0xe84da28128a48dd5585d1abb1ba67276fdd70776',
'key': '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501207',
}, {
'address': '0xcc036143c68a7a9a41558eae739b428ecde5ef66',
'key': '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501208',
}, {
'address': '0xe2b3204f29ab45d5fd074ff02ade098fbc381d42',
'key': '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501209',
},
];

/**
* Get an array between 1-10 length of accounts used in testrpc.
*/
export function getRandomAccounts (amount) {
if (amount > 10) throw new Error('Cant require more than 10 random accounts');

function shuffle (a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
return shuffle(accounts).slice(0, amount);
}
124 changes: 124 additions & 0 deletions test/token/ERC20/ERC20Channel.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import latestTime from '../../helpers/latestTime';
import { increaseTimeTo, duration } from '../../helpers/increaseTime';
import EVMRevert from '../../helpers/EVMRevert';
import { getRandomAccounts } from '../../helpers/accounts';
var ethUtils = require('ethereumjs-util');

var BigNumber = web3.BigNumber;

require('chai')
.use(require('chai-as-promised'))
.use(require('chai-bignumber')(BigNumber))
.should();

var StandardTokenMock = artifacts.require('StandardTokenMock.sol');
var ERC20Channel = artifacts.require('ERC20Channel.sol');
var ECRecovery = artifacts.require('ECRecovery.sol');

contract('ERC20Channel', function () {
var token;
var tokenChannel;

// Get random signer and receiver accounts to be used in the tests
const randomAccounts = getRandomAccounts(2);
const receiver = randomAccounts[0].address;
const sender = randomAccounts[1].address;
const receiverPrivateKey = randomAccounts[0].key;
const senderPrivateKey = randomAccounts[1].key;

// Sign a message with a private key, it returns the signature in rpc format
function signMsg (msg, pvKey) {
const sig = ethUtils.ecsign(ethUtils.toBuffer(msg), ethUtils.toBuffer(pvKey));
return ethUtils.toRpcSig(sig.v, sig.r, sig.s);
}

beforeEach(async function () {
const ecrecovery = await ECRecovery.new();
ERC20Channel.link('ECRecovery', ecrecovery.address);
token = await StandardTokenMock.new(sender, 100);
});

it('create channel', async function () {
tokenChannel = await ERC20Channel.new(token.address, receiver, duration.days(1), { from: sender });
await token.transfer(tokenChannel.address, 60, { from: sender });
const channelInfo = await tokenChannel.getInfo();
assert.equal(parseInt(channelInfo[0]), 60);
assert.equal(parseInt(channelInfo[1]), 0);
assert.equal(parseInt(channelInfo[2]), 0);
});

it('create channel and close it with a mutual agreement from sender', async function () {
tokenChannel = await ERC20Channel.new(token.address, receiver, duration.days(1), { from: sender });
await token.transfer(tokenChannel.address, 30, { from: sender });
const hash = await tokenChannel.generateBalanceHash(20);
const senderSig = signMsg(hash, senderPrivateKey);
assert.equal(sender,
await tokenChannel.getSignerOfBalanceHash(20, senderSig)
);
const senderHash = await tokenChannel.generateKeccak256(senderSig);
const closingSig = signMsg(senderHash, receiverPrivateKey);
const channelInfo = await tokenChannel.getInfo();
assert.equal(parseInt(channelInfo[0]), 30);
assert.equal(parseInt(channelInfo[1]), 0);
assert.equal(parseInt(channelInfo[2]), 0);
await tokenChannel.cooperativeClose(20, senderSig, closingSig, { from: receiver });
(await token.balanceOf(sender)).should.be.bignumber
.equal(80);
(await token.balanceOf(receiver)).should.be.bignumber
.equal(20);
});

it('create channel and close it with a mutual agreement from receiver', async function () {
tokenChannel = await ERC20Channel.new(token.address, receiver, duration.days(1), { from: sender });
await token.transfer(tokenChannel.address, 30, { from: sender });
const hash = await tokenChannel.generateBalanceHash(20);
const senderSig = signMsg(hash, senderPrivateKey);
assert.equal(sender,
await tokenChannel.getSignerOfBalanceHash(20, senderSig)
);
const senderHash = await tokenChannel.generateKeccak256(senderSig);
const closingSig = signMsg(senderHash, receiverPrivateKey);
await tokenChannel.cooperativeClose(20, senderSig, closingSig, { from: sender });
(await token.balanceOf(sender)).should.be.bignumber
.equal(80);
(await token.balanceOf(receiver)).should.be.bignumber
.equal(20);
});

it('create channel and close it from sender with uncooperativeClose', async function () {
tokenChannel = await ERC20Channel.new(token.address, receiver, duration.days(1), { from: sender });
await token.transfer(tokenChannel.address, 30, { from: sender });
await tokenChannel.uncooperativeClose(10, { from: sender });
const channelInfo = await tokenChannel.getInfo();
assert.equal(parseInt(channelInfo[0]), 30);
assert.equal(parseInt(channelInfo[1]), latestTime() + duration.days(1));
assert.equal(parseInt(channelInfo[2]), 10);
await tokenChannel.closeChannel({ from: sender })
.should.be.rejectedWith(EVMRevert);
await increaseTimeTo(latestTime() + duration.days(2));
await tokenChannel.closeChannel({ from: sender });
(await token.balanceOf(sender)).should.be.bignumber
.equal(90);
(await token.balanceOf(receiver)).should.be.bignumber
.equal(10);
});

it('create channel and close it from receiver after sender challenge with different balance', async function () {
tokenChannel = await ERC20Channel.new(token.address, receiver, duration.days(1), { from: sender });
await token.transfer(tokenChannel.address, 30, { from: sender });
const hash = await tokenChannel.generateBalanceHash(20);
const senderSig = signMsg(hash, senderPrivateKey);
assert.equal(sender,
await tokenChannel.getSignerOfBalanceHash(20, senderSig)
);
const senderHash = await tokenChannel.generateKeccak256(senderSig);
const closingSig = signMsg(senderHash, receiverPrivateKey);
await tokenChannel.uncooperativeClose(10, { from: sender });
await increaseTimeTo(latestTime() + 10);
await tokenChannel.cooperativeClose(20, senderSig, closingSig, { from: receiver });
(await token.balanceOf(sender)).should.be.bignumber
.equal(80);
(await token.balanceOf(receiver)).should.be.bignumber
.equal(20);
});
});

0 comments on commit 9c42aa9

Please sign in to comment.