new contracts for L2 (#25)

* use block relay instead of block

* constructor => init

* copied correct IERC20

* single instance contract

* test requestPayment

* test topup/withdraw

* add new account check

* add account/nutberry handling

* implement blockrelay

* make compatible with Ethersjs 5

* added info functionality

* fix typo

* add wallet creation/topup/withdraw

* implement payment method

* extract utility function

* add OVM testing support
This commit is contained in:
Bitgamma 2020-08-25 11:05:38 +02:00 committed by GitHub
parent 2df1e4ed2e
commit 711fda63bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 12119 additions and 326 deletions

View File

@ -0,0 +1,6 @@
pragma solidity >=0.5.0 <0.7.0;
interface IBlockRelay {
function getNumber() external view returns (uint256);
function getHash(uint256) external view returns (bytes32);
}

View File

@ -73,4 +73,4 @@ interface IERC20 {
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}
}

View File

@ -2,7 +2,7 @@ pragma solidity >=0.5.0 <0.7.0;
import "./IERC20.sol";
contract ERC20Detailed is IERC20 {
abstract contract ERC20Detailed is IERC20 {
string private _name;
string private _symbol;
uint8 private _decimals;

View File

@ -1,8 +1,8 @@
pragma solidity >=0.5.0 <0.7.0;
interface KeycardRegistry {
function register(address _owner, address _keycard) external;
function unregister(address _owner, address _keycard) external;
function setOwner(address _oldOwner, address _newOwner) external;
function setKeycard(address _oldKeycard, address _newKeycard) external;
pragma solidity >=0.5.0 <0.7.0;
interface KeycardRegistry {
function register(address _owner, address _keycard) external;
function unregister(address _owner, address _keycard) external;
function setOwner(address _oldOwner, address _newOwner) external;
function setKeycard(address _oldKeycard, address _newKeycard) external;
}

View File

@ -1,173 +1,178 @@
pragma solidity >=0.5.0 <0.7.0;
pragma experimental ABIEncoderV2;
import "./KeycardRegistry.sol";
import "./IERC20.sol";
contract KeycardWallet {
event NewPayment(uint256 blockNumber, address to, address currency, uint256 amount);
//TODO: replace with chainid opcode
// uint256 constant chainId = 1;
uint256 constant chainId = 3;
// uint256 constant chainId = 5;
// uint256 constant chainId = 1337;
// must be less than 256, because the hash of older blocks cannot be retrieved
uint256 constant maxTxDelayInBlocks = 10;
struct Payment {
uint256 blockNumber;
bytes32 blockHash;
address currency;
uint256 amount;
address to;
}
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
bytes32 constant PAYMENT_TYPEHASH = keccak256("Payment(uint256 blockNumber,bytes32 blockHash,address currency,uint256 amount,address to)");
bytes32 DOMAIN_SEPARATOR;
address public register;
address public owner;
address public keycard;
mapping(address => uint) public tokenMaxTxAmount;
uint256 public lastUsedBlockNum;
uint256 public minBlockDistance;
modifier onlyOwner() {
require(msg.sender == owner, "owner required");
_;
}
constructor(address _owner, address _keycard, address _register, uint256 _minBlockDistance, address _token, uint256 _tokenMaxTxAmount) public {
owner = _owner == address(0) ? msg.sender : _owner;
keycard = _keycard;
register = address(0);
minBlockDistance = _minBlockDistance;
_setRegister(_register);
lastUsedBlockNum = block.number;
tokenMaxTxAmount[_token] = _tokenMaxTxAmount;
}
function _setRegister(address _register) internal {
if (register != address(0)) {
KeycardRegistry(register).unregister(owner, keycard);
}
if (_register != address(0) && msg.sender != _register) {
KeycardRegistry(_register).register(owner, keycard);
}
register = _register;
DOMAIN_SEPARATOR = keccak256(abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256("KeycardWallet"),
keccak256("1"),
chainId,
register
));
}
function setRegister(address _register) public onlyOwner {
_setRegister(_register);
}
function setOwner(address _owner) public onlyOwner {
if (register != address(0)) {
KeycardRegistry(register).setOwner(owner, _owner);
}
owner = _owner;
}
function setKeycard(address _keycard) public onlyOwner {
if (register != address(0)) {
KeycardRegistry(register).setKeycard(keycard, _keycard);
}
keycard = _keycard;
}
function setMinBlockDistance(uint256 _minBlockDistance) public onlyOwner {
minBlockDistance = _minBlockDistance;
}
function setTokenMaxTXAmount(address _token, uint256 _maxTxAmount) public onlyOwner {
tokenMaxTxAmount[_token] = _maxTxAmount;
}
function hash(Payment memory _payment) internal pure returns (bytes32) {
return keccak256(abi.encode(
PAYMENT_TYPEHASH,
_payment.blockNumber,
_payment.blockHash,
_payment.currency,
_payment.amount,
_payment.to
));
}
function verify(Payment memory _payment, bytes memory _sig) internal view returns (bool) {
require(_sig.length == 65, "bad signature length");
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(_sig, 32))
s := mload(add(_sig, 64))
v := byte(0, mload(add(_sig, 96)))
}
if (v < 27) {
v += 27;
}
require(v == 27 || v == 28, "signature version doesn't match");
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hash(_payment)
));
return ecrecover(digest, v, r, s) == keycard;
}
function requestPayment(Payment memory _payment, bytes memory _signature) public {
// check that a keycard address has been set
require(keycard != address(0), "keycard address not set");
// verify the signer
require(verify(_payment, _signature), "signer is not the keycard");
// check that the block number used for signing is less than the block number
require(_payment.blockNumber < block.number, "transaction cannot be in the future");
// check that the block number used is not too old
require(_payment.blockNumber >= (block.number - maxTxDelayInBlocks), "transaction too old");
// check that the block number is not too near to the last one in which a tx has been processed
require(_payment.blockNumber >= (lastUsedBlockNum + minBlockDistance), "cooldown period not expired yet");
// check that the blockHash is valid
require(_payment.blockHash == blockhash(_payment.blockNumber), "invalid block hash");
// check that _payment.amount is not greater than the maxTxValue for this currency
require(_payment.amount <= tokenMaxTxAmount[_payment.currency], "amount not allowed");
// check that balance is enough for this payment
require(IERC20(_payment.currency).balanceOf(address(this)) >= _payment.amount, "balance is not enough");
// transfer token
require(IERC20(_payment.currency).transfer(_payment.to, _payment.amount), "transfer failed");
// set new baseline block for checks
lastUsedBlockNum = block.number;
emit NewPayment(_payment.blockNumber, _payment.to, _payment.currency, _payment.amount);
}
}
pragma solidity >=0.5.0 <0.7.0;
pragma experimental ABIEncoderV2;
import "./KeycardRegistry.sol";
import "./IERC20.sol";
import "./IBlockRelay.sol";
contract KeycardWallet {
event NewPayment(uint256 blockNumber, address to, address currency, uint256 amount);
//TODO: replace with chainid opcode
// uint256 constant chainId = 1;
uint256 constant chainId = 3;
// uint256 constant chainId = 5;
// uint256 constant chainId = 1337;
// must be less than 256, because the hash of older blocks cannot be retrieved
uint256 constant maxTxDelayInBlocks = 10;
struct Payment {
uint256 blockNumber;
bytes32 blockHash;
address currency;
uint256 amount;
address to;
}
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
bytes32 constant PAYMENT_TYPEHASH = keccak256("Payment(uint256 blockNumber,bytes32 blockHash,address currency,uint256 amount,address to)");
bytes32 DOMAIN_SEPARATOR;
address public register;
address public blockRelay;
address public owner;
address public keycard;
mapping(address => uint) public tokenMaxTxAmount;
uint256 public lastUsedBlockNum;
uint256 public minBlockDistance;
modifier onlyOwner() {
require(msg.sender == owner, "owner required");
_;
}
function init(address _owner, address _keycard, address _register, address _blockRelay, uint256 _minBlockDistance, address _token, uint256 _tokenMaxTxAmount) public {
require(owner == address(0), "this function can only be invoked once");
owner = _owner == address(0) ? msg.sender : _owner;
keycard = _keycard;
register = address(0);
blockRelay = _blockRelay;
minBlockDistance = _minBlockDistance;
_setRegister(_register);
lastUsedBlockNum = IBlockRelay(blockRelay).getNumber();
tokenMaxTxAmount[_token] = _tokenMaxTxAmount;
}
function _setRegister(address _register) internal {
if (register != address(0)) {
KeycardRegistry(register).unregister(owner, keycard);
}
if (_register != address(0) && msg.sender != _register) {
KeycardRegistry(_register).register(owner, keycard);
}
register = _register;
DOMAIN_SEPARATOR = keccak256(abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256("KeycardWallet"),
keccak256("1"),
chainId,
register
));
}
function setRegister(address _register) public onlyOwner {
_setRegister(_register);
}
function setOwner(address _owner) public onlyOwner {
if (register != address(0)) {
KeycardRegistry(register).setOwner(owner, _owner);
}
owner = _owner;
}
function setKeycard(address _keycard) public onlyOwner {
if (register != address(0)) {
KeycardRegistry(register).setKeycard(keycard, _keycard);
}
keycard = _keycard;
}
function setMinBlockDistance(uint256 _minBlockDistance) public onlyOwner {
minBlockDistance = _minBlockDistance;
}
function setTokenMaxTXAmount(address _token, uint256 _maxTxAmount) public onlyOwner {
tokenMaxTxAmount[_token] = _maxTxAmount;
}
function hash(Payment memory _payment) internal pure returns (bytes32) {
return keccak256(abi.encode(
PAYMENT_TYPEHASH,
_payment.blockNumber,
_payment.blockHash,
_payment.currency,
_payment.amount,
_payment.to
));
}
function verify(Payment memory _payment, bytes memory _sig) internal view returns (bool) {
require(_sig.length == 65, "bad signature length");
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(_sig, 32))
s := mload(add(_sig, 64))
v := byte(0, mload(add(_sig, 96)))
}
if (v < 27) {
v += 27;
}
require(v == 27 || v == 28, "signature version doesn't match");
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hash(_payment)
));
return ecrecover(digest, v, r, s) == keycard;
}
function requestPayment(Payment memory _payment, bytes memory _signature) public {
// check that a keycard address has been set
require(keycard != address(0), "keycard address not set");
// verify the signer
require(verify(_payment, _signature), "signer is not the keycard");
// check that the block number used for signing is less than the block number
require(_payment.blockNumber < IBlockRelay(blockRelay).getNumber(), "transaction cannot be in the future");
// check that the block number used is not too old
require(_payment.blockNumber >= (IBlockRelay(blockRelay).getNumber() - maxTxDelayInBlocks), "transaction too old");
// check that the block number is not too near to the last one in which a tx has been processed
require(_payment.blockNumber >= (lastUsedBlockNum + minBlockDistance), "cooldown period not expired yet");
// check that the blockHash is valid
require(_payment.blockHash == IBlockRelay(blockRelay).getHash(_payment.blockNumber), "invalid block hash");
// check that _payment.amount is not greater than the maxTxValue for this currency
require(_payment.amount <= tokenMaxTxAmount[_payment.currency], "amount not allowed");
// check that balance is enough for this payment
require(IERC20(_payment.currency).balanceOf(address(this)) >= _payment.amount, "balance is not enough");
// transfer token
require(IERC20(_payment.currency).transfer(_payment.to, _payment.amount), "transfer failed");
// set new baseline block for checks
lastUsedBlockNum = IBlockRelay(blockRelay).getNumber();
emit NewPayment(_payment.blockNumber, _payment.to, _payment.currency, _payment.amount);
}
}

View File

@ -1,88 +1,95 @@
pragma solidity >=0.5.0 <0.7.0;
pragma experimental ABIEncoderV2;
import "./KeycardWallet.sol";
import "./KeycardRegistry.sol";
contract KeycardWalletFactory is KeycardRegistry {
mapping(address => address[]) public ownersWallets;
mapping(address => address) public keycardsWallets;
address public currency;
event NewWallet(KeycardWallet wallet);
constructor(address _currency) public {
currency = _currency;
}
function create(address _keycard, bool _keycardIsOwner, uint256 _minBlockDistance, uint256 _txMaxAmount) public {
address owner = _keycardIsOwner ? _keycard : msg.sender;
require(keycardsWallets[_keycard] == address(0), "the keycard is already associated to a wallet");
KeycardWallet wallet = new KeycardWallet(owner, _keycard, address(this), _minBlockDistance, currency, _txMaxAmount);
ownersWallets[owner].push(address(wallet));
keycardsWallets[_keycard] = address(wallet);
emit NewWallet(wallet);
}
function addressFind(address[] storage _arr, address _a) internal view returns (uint) {
for (uint i = 0; i < _arr.length; i++){
if (_arr[i] == _a) {
return i;
}
}
revert("address not found");
}
function addressDelete(address[] storage _arr, uint _idx) internal {
_arr[_idx] = _arr[_arr.length-1];
_arr.length--;
}
function setOwner(address _oldOwner, address _newOwner) public {
uint idx = addressFind(ownersWallets[_oldOwner], msg.sender);
ownersWallets[_newOwner].push(ownersWallets[_oldOwner][idx]);
addressDelete(ownersWallets[_oldOwner], idx);
}
function setKeycard(address _oldKeycard, address _newKeycard) public {
address wallet = keycardsWallets[_oldKeycard];
require(wallet == msg.sender, "only the registered wallet can call this");
require(keycardsWallets[_newKeycard] == address(0), "the keycard already has a wallet");
keycardsWallets[_newKeycard] = wallet;
delete keycardsWallets[_oldKeycard];
}
function unregisterFromOwner(address _wallet, address _keycard) public {
uint idx = addressFind(ownersWallets[msg.sender], _wallet);
require(_wallet == keycardsWallets[_keycard], "owner required");
addressDelete(ownersWallets[msg.sender], idx);
delete keycardsWallets[_keycard];
}
function countWalletsForOwner(address owner) public view returns (uint) {
return ownersWallets[owner].length;
}
function unregister(address _owner, address _keycard) public {
uint idx = addressFind(ownersWallets[_owner], msg.sender);
require(ownersWallets[_owner][idx] == keycardsWallets[_keycard], "only the associated keycard can be deassociated");
addressDelete(ownersWallets[_owner], idx);
delete keycardsWallets[_keycard];
}
function register(address _owner, address _keycard) public {
require(keycardsWallets[_keycard] == address(0), "the keycard already has a wallet");
ownersWallets[_owner].push(msg.sender);
keycardsWallets[_keycard] = msg.sender;
}
}
pragma solidity >=0.5.0 <0.7.0;
pragma experimental ABIEncoderV2;
import "./KeycardWallet.sol";
import "./KeycardRegistry.sol";
contract KeycardWalletFactory is KeycardRegistry {
mapping(address => address[]) public ownersWallets;
mapping(address => address) public keycardsWallets;
address public currency;
address public blockRelay;
address public owner;
event NewWallet(KeycardWallet wallet);
function init(address _currency, address _blockRelay) public {
require(owner == address(0), "this function can only be invoked once");
owner = msg.sender;
currency = _currency;
blockRelay = _blockRelay;
}
function create(address _keycard, bool _keycardIsOwner, uint256 _minBlockDistance, uint256 _txMaxAmount) public {
address owner = _keycardIsOwner ? _keycard : msg.sender;
require(keycardsWallets[_keycard] == address(0), "the keycard is already associated to a wallet");
KeycardWallet wallet = new KeycardWallet();
ownersWallets[owner].push(address(wallet));
keycardsWallets[_keycard] = address(wallet);
wallet.init(owner, _keycard, address(this), blockRelay, _minBlockDistance, currency, _txMaxAmount);
emit NewWallet(wallet);
}
function addressFind(address[] storage _arr, address _a) internal view returns (uint) {
for (uint i = 0; i < _arr.length; i++){
if (_arr[i] == _a) {
return i;
}
}
revert("address not found");
}
function addressDelete(address[] storage _arr, uint _idx) internal {
_arr[_idx] = _arr[_arr.length-1];
_arr.length--;
}
function setOwner(address _oldOwner, address _newOwner) public {
uint idx = addressFind(ownersWallets[_oldOwner], msg.sender);
ownersWallets[_newOwner].push(ownersWallets[_oldOwner][idx]);
addressDelete(ownersWallets[_oldOwner], idx);
}
function setKeycard(address _oldKeycard, address _newKeycard) public {
address wallet = keycardsWallets[_oldKeycard];
require(wallet == msg.sender, "only the registered wallet can call this");
require(keycardsWallets[_newKeycard] == address(0), "the keycard already has a wallet");
keycardsWallets[_newKeycard] = wallet;
delete keycardsWallets[_oldKeycard];
}
function unregisterFromOwner(address _wallet, address _keycard) public {
uint idx = addressFind(ownersWallets[msg.sender], _wallet);
require(_wallet == keycardsWallets[_keycard], "owner required");
addressDelete(ownersWallets[msg.sender], idx);
delete keycardsWallets[_keycard];
}
function countWalletsForOwner(address owner) public view returns (uint) {
return ownersWallets[owner].length;
}
function unregister(address _owner, address _keycard) public {
uint idx = addressFind(ownersWallets[_owner], msg.sender);
require(ownersWallets[_owner][idx] == keycardsWallets[_keycard], "only the associated keycard can be deassociated");
addressDelete(ownersWallets[_owner], idx);
delete keycardsWallets[_keycard];
}
function register(address _owner, address _keycard) public {
require(keycardsWallets[_keycard] == address(0), "the keycard already has a wallet");
ownersWallets[_owner].push(msg.sender);
keycardsWallets[_keycard] = msg.sender;
}
}

View File

@ -1,27 +1,28 @@
pragma solidity ^0.5.0;
contract MerchantsRegistry {
address public owner;
mapping(address => bool) public merchants;
modifier onlyOwner() {
require(msg.sender == owner, "owner required");
_;
}
constructor() public {
owner = msg.sender;
}
function setOwner(address newOwner) public onlyOwner {
owner = newOwner;
}
function addMerchant(address merchantAddress) public onlyOwner {
merchants[merchantAddress] = true;
}
function removeMerchant(address merchantAddress) public onlyOwner {
merchants[merchantAddress] = false;
}
}
pragma solidity ^0.5.0;
contract MerchantsRegistry {
address public owner;
mapping(address => bool) public merchants;
modifier onlyOwner() {
require(msg.sender == owner, "owner required");
_;
}
function init() public {
require(owner == address(0), "this function can only be invoked once");
owner = msg.sender;
}
function setOwner(address newOwner) public onlyOwner {
owner = newOwner;
}
function addMerchant(address merchantAddress) public onlyOwner {
merchants[merchantAddress] = true;
}
function removeMerchant(address merchantAddress) public onlyOwner {
merchants[merchantAddress] = false;
}
}

View File

@ -0,0 +1,26 @@
pragma solidity >=0.5.0 <0.7.0;
import "./IBlockRelay.sol";
contract MockBlockRelay is IBlockRelay {
uint256 public lastBlock;
mapping (uint256 => bytes32) public hashes;
function getNumber() public override view returns (uint256) {
return lastBlock;
}
function getHash(uint256 num) public override view returns (bytes32) {
if ((num > lastBlock) || (lastBlock - num) > 255) {
return bytes32(0);
}
return hashes[num % 256];
}
function addBlock(uint256 num, bytes32 h) external {
require(num > lastBlock, "You can only add newer blocks");
hashes[num % 255] = h;
lastBlock = num;
}
}

View File

@ -1,30 +1,29 @@
pragma solidity >=0.5.0 <0.7.0;
pragma experimental ABIEncoderV2;
import "./IERC20.sol";
contract MockERC20 is IERC20 {
function totalSupply() public view returns (uint256) {
return 1000;
}
function balanceOf(address /*account*/) public view returns (uint256) {
return 1000;
}
function transfer(address /*recipient*/, uint256 amount) public returns (bool) {
return (amount <= 1000);
}
function allowance(address /*owner*/, address /*spender*/) public view returns (uint256) {
return 0;
}
function approve(address /*spender*/, uint256 /*amount*/) public returns (bool) {
return false;
}
function transferFrom(address /*sender*/, address /*recipient*/, uint256 /*amount*/) public returns (bool) {
return false;
}
pragma solidity >=0.5.0 <0.7.0;
import "./IERC20.sol";
contract MockERC20 is IERC20 {
function init() public {
totalSupply = 1000;
}
function balanceOf(address /*account*/) public view returns (uint256) {
return 1000;
}
function transfer(address /*recipient*/, uint256 amount) public returns (bool) {
return (amount <= 1000);
}
function allowance(address /*owner*/, address /*spender*/) public view returns (uint256) {
return 0;
}
function approve(address /*spender*/, uint256 /*amount*/) public returns (bool) {
return false;
}
function transferFrom(address /*sender*/, address /*recipient*/, uint256 /*amount*/) public returns (bool) {
return false;
}
}

3
status-pay/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build/
installed_contracts/
node_modules/

View File

@ -0,0 +1,41 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.7.0;
import "./IBlockRelay.sol";
contract BlockRelay is IBlockRelay {
uint256 public lastBlock;
mapping (uint256 => bytes32) public hashes;
address owner;
uint256 constant HISTORY_SIZE = 50;
function init(uint256 _firstBlock, bytes32 _firstHash) public {
require(owner == address(0), "already done");
owner = msg.sender;
addBlock(_firstBlock, _firstHash);
}
function getLast() public view returns (uint256) {
return lastBlock;
}
function getHash(uint256 num) public view returns (bytes32) {
if ((num > lastBlock) || (lastBlock - num) >= HISTORY_SIZE) {
return bytes32(0);
}
return hashes[num % HISTORY_SIZE];
}
function historySize() external view returns (uint256) {
return HISTORY_SIZE;
}
function addBlock(uint256 num, bytes32 h) public {
require(num > lastBlock, "You can only add newer blocks");
hashes[num % HISTORY_SIZE] = h;
lastBlock = num;
}
}

View File

@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.7.0;
import "./IERC20.sol";
contract ERC20 is IERC20 {
uint256 constant private MAX_UINT256 = 2**256 - 1;
mapping (address => uint256) public balances;
mapping (address => mapping (address => uint256)) public allowed;
uint256 public totalSupply;
function init(uint256 _initialAmount) public {
require(totalSupply == 0, "already done");
balances[msg.sender] = _initialAmount;
totalSupply = _initialAmount;
}
function transfer(address _to, uint256 _value) public returns (bool success) {
require(balances[msg.sender] >= _value, "balance exceeded");
balances[msg.sender] -= _value;
balances[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
uint256 allowance = allowed[_from][msg.sender];
require(balances[_from] >= _value && allowance >= _value, "balance or allowance exceeded");
balances[_to] += _value;
balances[_from] -= _value;
if (allowance < MAX_UINT256) {
allowed[_from][msg.sender] -= _value;
}
emit Transfer(_from, _to, _value);
return true;
}
function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
function approve(address _spender, uint256 _value) public returns (bool success) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
function allowance(address _owner, address _spender) public view returns (uint256 remaining) {
return allowed[_owner][_spender];
}
}

View File

@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.7.0;
import "./ERC20.sol";
contract ERC20Detailed is ERC20 {
string private _name;
string private _symbol;
uint8 private _decimals;
/**
* @dev Sets the values for `name`, `symbol`, and `decimals`. All three of
* these values are immutable: they can only be set once during
* construction.
*/
constructor (string memory name, string memory symbol, uint8 decimals) public {
_name = name;
_symbol = symbol;
_decimals = decimals;
}
/**
* @dev Returns the name of the token.
*/
function name() public view returns (string memory) {
return _name;
}
/**
* @dev Returns the symbol of the token, usually a shorter version of the
* name.
*/
function symbol() public view returns (string memory) {
return _symbol;
}
/**
* @dev Returns the number of decimals used to get its user representation.
* For example, if `decimals` equals `2`, a balance of `505` tokens should
* be displayed to a user as `5,05` (`505 / 10 ** 2`).
*
* Tokens usually opt for a value of 18, imitating the relationship between
* Ether and Wei.
*
* NOTE: This information is only used for _display_ purposes: it in
* no way affects any of the arithmetic of the contract, including
* {IERC20-balanceOf} and {IERC20-transfer}.
*/
function decimals() public view returns (uint8) {
return _decimals;
}
}

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.7.0;
pragma experimental ABIEncoderV2;
library EVMUtils {
function eip712Hash(bytes32 _domainSeparator, bytes32 _messageHash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked(
"\x19\x01",
_domainSeparator,
_messageHash
));
}
function recoverSigner(bytes32 _digest, bytes memory _sig) internal pure returns (address) {
require(_sig.length == 65, "bad signature length");
bytes32 r;
bytes32 s;
uint8 v;
// solium-disable-next-line security/no-inline-assembly
assembly {
r := mload(add(_sig, 32))
s := mload(add(_sig, 64))
v := byte(0, mload(add(_sig, 96)))
}
if (v < 27) {
v += 27;
}
require(v == 27 || v == 28, "signature version doesn't match");
return ecrecover(_digest, v, r, s);
}
function getChainID() internal pure returns (uint256) {
uint256 id;
assembly {
id := chainid()
}
return id;
}
}

View File

@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.7.0;
interface IBlockRelay {
function getLast() external view returns (uint256);
function getHash(uint256 /*blockNum*/) external view returns (bytes32);
function historySize() external view returns (uint256);
}

View File

@ -0,0 +1,77 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.7.0;
/**
* @dev Interface of the ERC20 standard as defined in the EIP. Does not include
* the optional functions; to access them see {ERC20Detailed}.
*/
interface IERC20 {
/**
* @dev Returns the amount of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the amount of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves `amount` tokens from the caller's account to `recipient`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 amount) external returns (bool);
/**
* @dev Moves `amount` tokens from `sender` to `recipient` using the
* allowance mechanism. `amount` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.21 <0.7.0;
contract Migrations {
address public owner;
uint public last_completed_migration;
constructor() public {
owner = msg.sender;
}
modifier restricted() {
if (msg.sender == owner) _;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
}

View File

@ -0,0 +1,186 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.7.0;
pragma experimental ABIEncoderV2;
import "./IERC20.sol";
import "./IBlockRelay.sol";
import "./EVMUtils.sol";
contract StatusPay {
event NewPayment(address to, uint256 amount);
struct Payment {
uint256 blockNumber;
bytes32 blockHash;
uint256 amount;
address to;
}
struct Account {
bool exists;
uint256 balance;
uint256 lastUsedBlock;
uint256 minBlockDistance;
uint256 maxTxAmount;
}
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
bytes32 constant PAYMENT_TYPEHASH = keccak256("Payment(uint256 blockNumber,bytes32 blockHash,uint256 amount,address to)");
bytes32 DOMAIN_SEPARATOR;
uint256 public maxTxDelayInBlocks;
IBlockRelay public blockRelay;
IERC20 public token;
address public networkOwner;
mapping(address => address) public keycards;
mapping(address => Account) public accounts;
function init(address _blockRelay, address _token, uint256 _maxDelayInBlocks) public {
require(networkOwner == address(0), "already done");
networkOwner = msg.sender;
blockRelay = IBlockRelay(_blockRelay);
token = IERC20(_token);
require(_maxDelayInBlocks <= blockRelay.historySize(), "max delay cannot be more than history size");
maxTxDelayInBlocks = _maxDelayInBlocks;
DOMAIN_SEPARATOR = keccak256(abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256("StatusPay"),
keccak256("1"),
EVMUtils.getChainID(),
address(this)
));
}
function createAccount(address _owner, address _keycard, uint256 _minBlockDistance, uint256 _maxTxAmount) public {
require(networkOwner == msg.sender, "only the network owner can create accounts");
Account storage account = accounts[_owner];
require(!account.exists, "already exists");
if (_keycard != address(0)) {
_addKeycard(_keycard, _owner);
}
account.exists = true;
account.lastUsedBlock = blockRelay.getLast();
account.minBlockDistance = _minBlockDistance;
account.maxTxAmount = _maxTxAmount;
}
function transferAccount(address _newOwner, address _keycard) public {
Account storage oldAcc = accounts[msg.sender];
require(oldAcc.exists, "account to transfer does not exist");
Account storage newAcc = accounts[_newOwner];
require(!newAcc.exists, "the new owner already has an account");
newAcc.exists = true;
newAcc.balance = oldAcc.balance;
newAcc.lastUsedBlock = oldAcc.lastUsedBlock;
newAcc.minBlockDistance = oldAcc.minBlockDistance;
newAcc.maxTxAmount = oldAcc.maxTxAmount;
oldAcc.exists = false;
oldAcc.balance = 0;
oldAcc.lastUsedBlock = 0;
oldAcc.minBlockDistance = 0;
oldAcc.maxTxAmount = 0;
if (_keycard != address(0)) {
_addKeycard(_keycard, _newOwner);
}
}
function addKeycard(address _keycard) public {
_addKeycard(_keycard, msg.sender);
}
function _addKeycard(address _keycard, address _owner) internal {
require(!accounts[keycards[_keycard]].exists, "keycard already assigned");
keycards[_keycard] = _owner;
}
function removeKeycard(address _keycard) public {
require(keycards[_keycard] == msg.sender, "keycard not owned");
keycards[_keycard] = address(0);
}
function topup(address _to, uint256 _amount) public {
Account storage topped = accounts[_to];
require(topped.exists, "account does not exist");
require(token.transferFrom(msg.sender, address(this), _amount), "transfer failed");
topped.balance += _amount;
}
function withdraw(address _to, uint256 _amount) public {
Account storage exiting = accounts[msg.sender];
require(exiting.exists, "account does not exist");
require(exiting.balance >= _amount, "not enough balance");
exiting.balance -= _amount;
require(token.transfer(_to, _amount), "transfer failed");
}
function requestPayment(Payment memory _payment, bytes memory _signature) public {
address signer = EVMUtils.recoverSigner(EVMUtils.eip712Hash(DOMAIN_SEPARATOR, hash(_payment)), _signature);
Account storage payer = accounts[keycards[signer]];
// allow direct payment without Keycard from owner
if (!payer.exists) {
payer = accounts[signer];
}
// check that a keycard is associated to this account
require(payer.exists, "no account for this Keycard");
// check that the payee exists
Account storage payee = accounts[_payment.to];
require(payee.exists, "payee account does not exist");
// check that _payment.amount is not greater than the maxTxValue for this currency
require(_payment.amount <= payer.maxTxAmount, "amount not allowed");
// check that balance is enough for this payment
require(payer.balance >= _payment.amount, "balance is not enough");
uint256 blockNumber = blockRelay.getLast();
// check that the block number used for signing is not newer than the block number
require(_payment.blockNumber <= blockNumber, "transaction cannot be in the future");
// check that the block number used is not too old
require(_payment.blockNumber > (blockNumber - maxTxDelayInBlocks), "transaction too old");
// check that the block number is not too near to the last one in which a tx has been processed
require(_payment.blockNumber >= (payer.lastUsedBlock + payer.minBlockDistance), "cooldown period not expired yet");
// check that the blockHash is valid
require(_payment.blockHash == blockRelay.getHash(_payment.blockNumber), "invalid block hash");
// this check is redundant but provideds a safety net if the oracle returns a 0 hash
require(_payment.blockHash != bytes32(0), "invalid block hash");
// perform transfer
payer.balance -= _payment.amount;
payee.balance += _payment.amount;
// set new baseline block for checks
payer.lastUsedBlock = blockNumber;
emit NewPayment(_payment.to, _payment.amount);
}
function hash(Payment memory _payment) internal pure returns (bytes32) {
return keccak256(abi.encode(
PAYMENT_TYPEHASH,
_payment.blockNumber,
_payment.blockHash,
_payment.amount,
_payment.to
));
}
}

View File

@ -0,0 +1,5 @@
const Migrations = artifacts.require("Migrations");
module.exports = function(deployer) {
deployer.deploy(Migrations);
};

29
status-pay/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "status-pay",
"version": "0.0.1",
"description": "Status Payment Network",
"scripts": {
"clean": "rimraf build",
"test": "truffle test",
"compile": "truffle compile",
"build:ovm": "DEBUG=info*,warn*,error* truffle compile --config truffle-config-ovm.js",
"test:ovm": "DEBUG=info*,warn* truffle test --config truffle-config-ovm.js",
"all:ovm": "yarn clean && yarn build:ovm && yarn test:ovm"
},
"license": "MIT",
"dependencies": {
"@eth-optimism/ovm-truffle-provider-wrapper": "^0.0.1-alpha.27",
"@eth-optimism/rollup-full-node": "^0.0.1-alpha.29",
"@eth-optimism/solc": "^0.5.16-alpha.0",
"solc": "0.5.16",
"eth-sig-util": "^2.5.3",
"ethers": "^5.0.7",
"minimist": "^1.2.5",
"truffle": "^5.1.34",
"truffle-hdwallet-provider": "^1.0.17"
},
"devDependencies": {
"bip39": "^3.0.2",
"ethereumjs-wallet": "^1.0.0"
}
}

View File

@ -0,0 +1,106 @@
const fs = require('fs');
const { ethers } = require("ethers");
const ethSigUtil = require('eth-sig-util');
const NUTBERRY_TX_TYPED_DATA = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
],
Transaction: [
{ name: 'to', type: 'address' },
{ name: 'nonce', type: 'uint256' },
{ name: 'data', type: 'bytes' },
],
},
primaryType: 'Transaction',
domain: {
name: 'NutBerry',
version: '2',
},
};
module.exports = class Account {
constructor(rpcURL) {
this.provider = new ethers.providers.JsonRpcProvider(rpcURL);
this.sender = null;
this.senderAddress = null;
}
async init(account, passfile) {
this.sender = await this.loadAccount(account, passfile);
this.senderAddress = await this.sender.getAddress();
}
async loadAccount(account, passfile) {
let json = fs.readFileSync(account, "utf-8");
let pass = fs.readFileSync(passfile, "utf-8").split("\n")[0].replace("\r", "");
return await ethers.Wallet.fromEncryptedJson(json, pass);
}
async sendDataTx(to, data) {
let signedTx = this.signTransaction({
to: to,
data: data,
nonce: await this.provider.getTransactionCount(this.senderAddress, 'pending')
});
let txHash = await this.provider.send('eth_sendRawTransaction', [signedTx]);
return await this.provider.getTransactionReceipt(txHash);
}
encodeTx(tx) {
function arrayify (val) {
let v = val;
if (typeof v === 'number' || typeof v === 'bigint') {
v = v.toString(16);
if (v.length % 2) {
v = `0x0${v}`;
} else {
v = `0x${v}`;
}
}
return Array.from(ethers.utils.arrayify(v));
}
const nonceBytes = arrayify(tx.nonce);
const calldataBytes = arrayify(tx.data);
let enc = arrayify(tx.v)
.concat(arrayify(tx.r))
.concat(arrayify(tx.s));
if (nonceBytes.length > 1 || nonceBytes[0] > 0xde) {
enc.push(0xff - nonceBytes.length);
enc = enc.concat(nonceBytes);
} else {
enc = enc.concat(nonceBytes);
}
enc = enc.concat(arrayify(tx.to));
if (calldataBytes.length >= 0xff) {
enc.push(0xff);
enc.push(calldataBytes.length >> 8);
enc.push(calldataBytes.length & 0xff);
} else {
enc.push(calldataBytes.length);
}
return ethers.utils.hexlify(enc.concat(calldataBytes));
}
signTransaction(tx) {
const sig = this.signTypedData(tx, NUTBERRY_TX_TYPED_DATA);
const { r, s, v } = ethers.utils.splitSignature(sig);
return this.encodeTx(Object.assign(tx, { r, s, v: v + 101 }));
}
signTypedData(message, typeInfo) {
const obj = Object.assign({ message: message }, typeInfo);
return ethSigUtil.signTypedData(Buffer.from(this.sender.privateKey.substring(2), "hex"), { data: obj });
}
}

View File

@ -0,0 +1,262 @@
const Account = require('./account.js');
const { ethers } = require("ethers");
const utils = require('./utils.js');
const parseArgs = require('minimist');
const BRIDGE_ADDRESS = '0xa9f96d8761aa05430d3ee2e7dff6b04978f13369';
const RPC_URL = `https://${BRIDGE_ADDRESS}.fly.dev`;
const argv = parseArgs(process.argv.slice(2), {string: ["token", "wallet", "blockrelay", "statuspay", "keycard", "merchant", "blockHash"], default: {"endpoint": RPC_URL, "token": "0x722dd3f80bac40c951b51bdd28dd19d435762180", maxTxDelayInBlocks: 10, minBlockDistance: 1, maxTxAmount: 1000000000000000000}});
const PAYMENT_TYPED_DATA = {
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" }
],
Payment: [
{ name: "blockNumber", type: "uint256" },
{ name: "blockHash", type: "bytes32" },
{ name: "amount", type: "uint256" },
{ name: "to", type: "address" }
]
},
primaryType: "Payment",
domain: {
name: "StatusPay",
version: "1",
chainId: 3,
verifyingContract: statusPay.address
}
};
async function loadSigner(argv, signer, passfile) {
let account = new Account(argv["endpoint"]);
if (argv[signer]) {
if (!argv[passfile]) {
console.error(`the --${passfile} option must be specified`);
process.exit(1);
}
await account.init(argv[signer], argv[passfile]);
} else if (argv["cmd"] != "info") {
console.error(`--${signer} is required`);
process.exit(1);
}
return account;
}
function getContract(argv, signer, optName, contractName) {
if (!argv[optName]) {
console.error(`the --${optName} option must be specified`);
process.exit(1);
}
return utils.loadContract(argv[optName], contractName, signer.provider);
}
function getBlockRelayContract(argv, signer) {
return getContract(argv, signer, "blockrelay", "BlockRelay");
}
function getERC20Contract(argv, signer) {
return getContract(argv, signer, "token", "ERC20Detailed");
}
function getStatusPayContract(argv, signer) {
return getContract(argv, signer, "statuspay", "StatusPay");
}
function getMandatory(argv, opt) {
if (!argv[opt]) {
console.error(`the --${opt} option must be specified`);
process.exit(1);
}
return argv[opt];
}
function getBlockOptions(argv) {
if (!argv["blockHash"] || !argv["blockNumber"]) {
console.error(`the --blockHash and --blockNumber options must be specified`);
process.exit(1);
}
return {blockNumber: argv["blockNumber"], blockHash: argv["blockHash"]};
}
async function initBlockRelay(argv, signer) {
let blockRelay = getBlockRelayContract(argv, signer);
let opts = getBlockOptions(argv);
await signer.sendDataTx(blockRelay.address, blockRelay.interface.encodeFunctionData("init", [opts.blockNumber, opts.blockHash]));
}
async function addBlock(argv, signer) {
let blockRelay = getBlockRelayContract(argv, signer);
let opts = getBlockOptions(argv);
await signer.sendDataTx(blockRelay.address, blockRelay.interface.encodeFunctionData("addBlock", [opts.blockNumber, opts.blockHash]));
}
async function initStatusPay(argv, signer) {
let blockRelay = getBlockRelayContract(argv, signer);
let erc20 = getERC20Contract(argv, signer);
let statusPay = getStatusPayContract(argv, signer);
let maxTxDelayInBlocks = argv["maxTxDelayInBlocks"];
await signer.sendDataTx(statusPay.address, statusPay.interface.encodeFunctionData("init", [blockRelay.address, erc20.address, maxTxDelayInBlocks]));
}
async function createWallet(argv, signer) {
let wallet = getMandatory(argv, "wallet");
let statusPay = getStatusPayContract(argv, signer);
let keycard = argv["keycard"] || ethers.constants.AddressZero;
await signer.sendDataTx(statusPay.address, statusPay.interface.encodeFunctionData("createAccount", [wallet, keycard, argv["minBlockDistance"], argv["maxTxAmount"]]));
}
async function topup(argv, signer) {
let wallet = getMandatory(argv, "wallet");
let amount = getMandatory(argv, "amount");
let statusPay = getStatusPayContract(argv, signer);
argv["token"] = await statusPay.token();
let erc20 = getERC20Contract(argv, signer);
await signer.sendDataTx(erc20.address, erc20.interface.encodeFunctionData("approve", [statusPay.address, amount]));
await signer.sendDataTx(statusPay.address, statusPay.interface.encodeFunctionData("topup", [wallet, amount]));
}
async function withdraw(argv, signer) {
let wallet = getMandatory(argv, "wallet");
let amount = getMandatory(argv, "amount");
let statusPay = getStatusPayContract(argv, signer);
await signer.sendDataTx(statusPay.address, statusPay.interface.encodeFunctionData("withdraw", [wallet, amount]));
}
async function payment(argv, signer) {
let keycardSigner = await loadSigner(argv, "keycardsigner", "keycardpassfile");
let statusPay = getStatusPayContract(argv, signer);
let merchant = getMandatory(argv, "merchant");
let amount = getMandatory(argv, "amount");
argv["blockrelay"] = await statusPay.blockRelay();
let blockRelay = getBlockRelayContract(argv, signer);
let blockNumber = await blockRelay.getLast();
let blockHash = await blockRelay.getHash(blockNumber);
let message = {blockNumber: blockNumber, blockHash: blockHash, amount: amount, to: merchant};
let sig = keycardSigner.signTypedData(message, PAYMENT_TYPED_DATA);
await signer.sendDataTx(statusPay.address, statusPay.interface.encodeFunctionData("requestPayment", [message, sig]));
}
async function info(argv, signer) {
let statusPay = getStatusPayContract(argv, signer);
console.log(`StatusPay (${statusPay.address})`);
console.log("==");
let networkOwner = await statusPay.networkOwner();
if (networkOwner == ethers.constants.AddressZero) {
console.log("This StatusPay instance has not yet been initialized");
} else {
let additionalInfo = ((signer.senderAddress != null) && (signer.senderAddress.toLowerCase() == networkOwner.toLowerCase())) ? " (you)" : "";
console.log(`Network owner: ${networkOwner}${additionalInfo}`);
}
argv["token"] = await statusPay.token();
let erc20 = getERC20Contract(argv, signer);
argv["blockrelay"] = await statusPay.blockRelay();
let blockRelay = getBlockRelayContract(argv, signer);
let erc20Unit = await erc20.decimals();
let ownedTokens = ethers.utils.formatUnits(await erc20.balanceOf(statusPay.address), erc20Unit);
console.log(`Token: ${erc20.address}`);
console.log(`Block Relay: ${blockRelay.address}`);
console.log(`Max transaction delay in blocks: ${await statusPay.maxTxDelayInBlocks()}`);
console.log(`Deposited token amount: ${ownedTokens}\n`);
if (argv["keycard"]) {
argv["wallet"] = await statusPay.keycards(argv["keycard"]);
if (argv["wallet"] == ethers.constants.AddressZero) {
console.log(`Keycard ${argv["keycard"]} has no associated account\n`);
argv["wallet"] = null;
}
}
if (argv["wallet"]) {
let account = await statusPay.accounts(argv["wallet"]);
if (!account.exists) {
let additionalInfo = argv["keycard"] ? `, but Keycard ${argv["keycard"]} is associated to it` : "";
console.log(`Account ${argv["wallet"]} does not exist${additionalInfo}\n`);
} else {
console.log(`Account (${argv["wallet"]})`);
console.log("==");
if (argv["keycard"]) {
console.log(`Keycard (there can be others too): ${argv["keycard"]}`);
}
console.log(`Balance: ${ethers.utils.formatUnits(account.balance, erc20Unit)}`);
console.log(`Last payment on block: ${account.lastUsedBlock}`);
console.log(`Cool-off period in blocks: ${account.minBlockDistance}`);
console.log(`Max transaction amount: ${ethers.utils.formatUnits(account.maxTxAmount, erc20Unit)}\n`);
}
}
console.log(`Token (${erc20.address})`);
console.log("==");
console.log(`Name: ${await erc20.name()}`);
console.log(`Symbol: ${await erc20.symbol()}`);
console.log(`Total supply: ${ethers.utils.formatUnits(await erc20.totalSupply(), erc20Unit)}\n`);
let lastBlock = await blockRelay.getLast();
console.log(`BlockRelay (${blockRelay.address})`);
console.log("==");
console.log(`Last block: ${lastBlock}`);
console.log(`Last hash: ${await blockRelay.getHash(lastBlock)}`);
}
async function run() {
let signer = await loadSigner(argv, "signer", "passfile");
switch(argv["cmd"]) {
case "init-block-relay":
await initBlockRelay(argv, signer);
break;
case "add-block":
await addBlock(argv, signer);
break;
case "init-status-pay":
await initStatusPay(argv, signer);
break;
case "create-wallet":
await createWallet(argv, signer);
break;
case "topup":
await topup(argv, signer);
break;
case "withdraw":
await withdraw(argv, signer);
break;
case "payment":
await payment(argv, signer);
break;
case "info":
await info(argv, signer);
break;
default:
console.log("The --cmd option is mandatory. Possible commands: init-block-relay, add-block, init-status-pay, create-wallet, topup, withdraw, payment");
process.exit(1);
}
process.exit(0);
}
run();

View File

@ -0,0 +1,15 @@
const fs = require('fs');
const path = require('path');
const { ethers } = require('ethers');
const CONTRACTS_PATH="../build/contracts";
module.exports.loadContractFile = (fileName) => {
let content = fs.readFileSync(path.join(__dirname, CONTRACTS_PATH, fileName), "utf-8");
return content;
};
module.exports.loadContract = (address, contractName, signerOrProvider) => {
let content = this.loadContractFile(`${contractName}.json`);
let contract = JSON.parse(content);
return new ethers.Contract(address, contract.abi, signerOrProvider);
};

View File

@ -0,0 +1,243 @@
const ERC20 = artifacts.require('ERC20');
const BlockRelay = artifacts.require('BlockRelay');
const StatusPay = artifacts.require('StatusPay');
const bip39 = require('bip39');
const { hdkey } = require('ethereumjs-wallet');
const ethSigUtil = require('eth-sig-util');
let token, block, statusPay, keycardKey;
const zeroAddress = "0x0000000000000000000000000000000000000000";
const seed = bip39.mnemonicToSeedSync("candy maple cake sugar pudding cream honey rich smooth crumble sweet treat");
const hdk = hdkey.fromMasterSeed(seed);
const CHAIN_ID = 1; //for now 1
contract('StatusPay', (accounts) => {
const owner = accounts[0];
const keycard = accounts[1];
const merchant = accounts[2];
const network = accounts[3];
before(async () => {
keycardKey = deriveKey(1, keycard);
token = await ERC20.new({from: network});
block = await BlockRelay.new({from: network});
statusPay = await StatusPay.new({from: network});
await token.init(10000, {from: network});
await block.init(500, "0xbababababaabaabaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", {from: network});
await statusPay.init(block.address, token.address, 10, {from: network});
await token.transfer(owner, 100, {from: network});
});
it('requestPayment with inexistant account', async () => {
try {
await requestPaymentTest(10);
assert.fail("requestPayment should have failed");
} catch (err) {
assert.equal(err.reason, "no account for this Keycard");
}
});
it('creates buyer account', async () => {
await statusPay.createAccount(owner, keycard, 1, 10, {from: network});
assert.equal((await statusPay.accounts.call(owner)).balance.toNumber(), 0);
});
it('requestPayment with inexisting merchant', async () => {
try {
await requestPaymentTest(10);
assert.fail("requestPayment should have failed");
} catch (err) {
assert.equal(err.reason, "payee account does not exist");
}
});
it('creates merchant account', async () => {
await statusPay.createAccount(merchant, zeroAddress, 1, 1000, {from: network});
assert.equal((await statusPay.accounts.call(merchant)).balance.toNumber(), 0);
});
it('requestPayment with insufficient balance', async () => {
try {
await requestPaymentTest(10);
assert.fail("requestPayment should have failed");
} catch (err) {
assert.equal(err.reason, "balance is not enough");
}
});
it('topup account', async () => {
await token.approve(statusPay.address, 100, {from: owner});
await statusPay.topup(owner, 100, {from: owner});
await block.addBlock(501, "0xbababababaabaabaaaacaabaaaaaaadaaadcaaadaaaaaaacaaaaaaddeaaaaaaa", {from: network});
assert.equal((await token.balanceOf.call(owner)).toNumber(), 0);
assert.equal((await statusPay.accounts.call(owner)).balance.toNumber(), 100);
});
it('topup exceed ERC20 balance', async () => {
await token.approve(statusPay.address, 100, {from: owner});
try {
await statusPay.topup(owner, 100, {from: owner});
assert.fail("topup should have failed");
} catch (err) {
assert(err.reason == "transfer failed" || err.reason == "balance or allowance exceeded");
}
assert.equal((await token.balanceOf.call(owner)).toNumber(), 0);
assert.equal((await statusPay.accounts.call(owner)).balance.toNumber(), 100);
});
it('topup account non-existing account', async () => {
try {
await statusPay.topup(network, 100, {from: network});
assert.fail("topup should have failed");
} catch (err) {
assert.equal(err.reason, "account does not exist");
}
});
it('requestPayment over limit', async () => {
try {
await requestPaymentTest(11);
assert.fail("requestPayment should have failed");
} catch (err) {
assert.equal(err.reason, "amount not allowed");
}
});
it('requestPayment with block too old', async () => {
try {
await requestPaymentTest(10, (await block.getLast.call()).toNumber() - 10);
assert.fail("requestPayment should have failed");
} catch (err) {
assert.equal(err.reason, "transaction too old");
}
});
it('requestPayment with invalid hash', async () => {
try {
await requestPaymentTest(10, undefined, "0xbababababaabaabaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
assert.fail("requestPayment should have failed");
} catch (err) {
assert.equal(err.reason, "invalid block hash");
}
});
it('requestPayment', async () => {
const receipt = await requestPaymentTest(10);
const event = receipt.logs.find(element => element.event.match('NewPayment'));
assert.equal(event.args.to, merchant);
assert.equal(event.args.amount, 10);
assert.equal((await statusPay.accounts.call(owner)).balance.toNumber(), 90);
assert.equal((await statusPay.accounts.call(merchant)).balance.toNumber(), 10);
});
it('requestPayment without waiting for cooldown', async () => {
try {
await requestPaymentTest(10);
assert.fail("requestPayment should have failed");
} catch (err) {
assert.equal(err.reason, "cooldown period not expired yet");
}
});
it('requestPayment with block in the future', async () => {
try {
await requestPaymentTest(10, (await block.getLast.call()).toNumber() + 1);
assert.fail("requestPayment should have failed");
} catch (err) {
assert.equal(err.reason, "transaction cannot be in the future");
}
});
it('withdraw', async () => {
await statusPay.withdraw(owner, 80, {from: owner});
assert.equal((await token.balanceOf.call(owner)).toNumber(), 80);
assert.equal((await statusPay.accounts.call(owner)).balance.toNumber(), 10);
});
it('withdraw more than balance allows', async () => {
try {
await statusPay.withdraw(owner, 11, {from: owner});
assert.fail("withdraw should have failed");
} catch (err) {
assert.equal(err.reason, "not enough balance");
}
assert.equal((await token.balanceOf.call(owner)).toNumber(), 80);
assert.equal((await statusPay.accounts.call(owner)).balance.toNumber(), 10);
});
it('withdraw non existing account', async () => {
try {
await statusPay.withdraw(network, 10, {from: network});
assert.fail("withdraw should have failed");
} catch (err) {
assert.equal(err.reason, "account does not exist");
}
});
requestPaymentTest = async (value, blockNum, blockH) => {
const blockNumber = blockNum || (await block.getLast.call()).toNumber();
const blockHash = blockH || await block.getHash.call(blockNumber);
const message = {blockNumber: blockNumber, blockHash: blockHash, amount: value, to: merchant};
const sig = signPaymentRequest(keycardKey, message);
return await statusPay.requestPayment(message, sig, {from: merchant});
};
deriveKey = (index, expectedAddr) => {
const addrNode = hdk.derivePath("m/44'/60'/0'/0/" + index);
const generatedAddr = addrNode.getWallet().getAddressString();
assert.equal(generatedAddr.toLowerCase(), expectedAddr.toLowerCase());
return addrNode.getWallet().getPrivateKey();
};
signPaymentRequest = (signer, message) => {
let domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
{ name: "chainId", type: "uint256" },
{ name: "verifyingContract", type: "address" }
];
let payment = [
{ name: "blockNumber", type: "uint256" },
{ name: "blockHash", type: "bytes32" },
{ name: "amount", type: "uint256" },
{ name: "to", type: "address" }
];
let domainData = {
name: "StatusPay",
version: "1",
chainId: CHAIN_ID,
verifyingContract: statusPay.address
};
let data = {
types: {
EIP712Domain: domain,
Payment: payment
},
primaryType: "Payment",
domain: domainData,
message: message
};
return ethSigUtil.signTypedData(signer, { data: data });
};
});

View File

@ -0,0 +1,42 @@
const HDWalletProvider = require("truffle-hdwallet-provider");
const ProviderWrapper = require("@eth-optimism/ovm-truffle-provider-wrapper");
const mnemonic = "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat";
// Set this to the desired Execution Manager Address -- required for the transpiler
process.env.EXECUTION_MANAGER_ADDRESS = process.env.EXECUTION_MANAGER_ADDRESS || "0x6454c9d69a4721feba60e26a367bd4d56196ee7c";
const gasPrice = process.env.OVM_DEFAULT_GAS_PRICE || 0;
const gas = process.env.OVM_DEFAULT_GAS || 1000000000;
module.exports = {
contracts_build_directory: './build/contracts',
/**
* Note: Using the `test` network will start a local node at 'http://127.0.0.1:8545/'
*
* To run tests:
* $ truffle test ./truffle-tests/test-erc20.js --config truffle-config-ovm.js
*/
networks: {
test: {
network_id: 108,
networkCheckTimeout: 100000,
provider: function() {
return ProviderWrapper.wrapProviderAndStartLocalNode(new HDWalletProvider(mnemonic, "http://127.0.0.1:8545/", 0, 10));
},
gasPrice: gasPrice,
gas: gas,
},
},
// Set default mocha options here, use special reporters etc.
mocha: {
timeout: 100000
},
compilers: {
solc: {
// Add path to the solc-transpiler
version: "./node_modules/@eth-optimism/solc",
}
}
}

View File

@ -0,0 +1,91 @@
/**
* Use this file to configure your truffle project. It's seeded with some
* common settings for different networks and features like migrations,
* compilation and testing. Uncomment the ones you need or modify
* them to suit your project as necessary.
*
* More information about configuration can be found at:
*
* truffleframework.com/docs/advanced/configuration
*
* To deploy via Infura you'll need a wallet provider (like @truffle/hdwallet-provider)
* to sign your transactions before they're sent to a remote public node. Infura accounts
* are available for free at: infura.io/register.
*
* You'll also need a mnemonic - the twelve word phrase the wallet uses to generate
* public/private key pairs. If you're publishing your code to GitHub make sure you load this
* phrase from a file you've .gitignored so it doesn't accidentally become public.
*
*/
// const HDWalletProvider = require('@truffle/hdwallet-provider');
// const infuraKey = "fj4jll3k.....";
//
// const fs = require('fs');
// const mnemonic = fs.readFileSync(".secret").toString().trim();
module.exports = {
/**
* Networks define how you connect to your ethereum client and let you set the
* defaults web3 uses to send transactions. If you don't specify one truffle
* will spin up a development blockchain for you on port 9545 when you
* run `develop` or `test`. You can ask a truffle command to use a specific
* network from the command line, e.g
*
* $ truffle test --network <network-name>
*/
networks: {
// Useful for testing. The `development` name is special - truffle uses it by default
// if it's defined here and no other network is specified at the command line.
// You should run a client (like ganache-cli, geth or parity) in a separate terminal
// tab if you use this network and you must also set the `host`, `port` and `network_id`
// options below to some value.
//
// development: {
// host: "127.0.0.1", // Localhost (default: none)
// port: 8545, // Standard Ethereum port (default: none)
// network_id: "*", // Any network (default: none)
// },
// Another network with more advanced options...
// advanced: {
// port: 8777, // Custom port
// network_id: 1342, // Custom network
// gas: 8500000, // Gas sent with each transaction (default: ~6700000)
// gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei)
// from: <address>, // Account to send txs from (default: accounts[0])
// websockets: true // Enable EventEmitter interface for web3 (default: false)
// },
// Useful for deploying to a public network.
// NB: It's important to wrap the provider as a function.
// ropsten: {
// provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`),
// network_id: 3, // Ropsten's id
// gas: 5500000, // Ropsten has a lower block limit than mainnet
// confirmations: 2, // # of confs to wait between deployments. (default: 0)
// timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
// skipDryRun: true // Skip dry run before migrations? (default: false for public nets )
// },
// Useful for private networks
// private: {
// provider: () => new HDWalletProvider(mnemonic, `https://network.io`),
// network_id: 2111, // This network is yours, in the cloud.
// production: true // Treats this network as if it was a public net. (default: false)
// }
},
// Set default mocha options here, use special reporters etc.
mocha: {
// timeout: 100000
},
// Configure your compilers
compilers: {
solc: {
version: "./node_modules/solc",
}
}
}

10472
status-pay/yarn.lock Normal file

File diff suppressed because it is too large Load Diff