-
Notifications
You must be signed in to change notification settings - Fork 11.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
379 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |