Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add tests for TransferValidatorV3 #245

Open
wants to merge 8 commits into
base: with_payload
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions contracts/iotube/TokenCashier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ contract TokenCashier is Pausable {
tokenSafes = _tokenSafes;
}

function() external {
revert();
}
// function() external {
// revert();
// }

function count(address _token) public view returns (uint256) {
return counts[_token];
Expand Down
111 changes: 111 additions & 0 deletions contracts/iotube/TokenCashierV3.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
pragma solidity <6.0 >=0.4.24;

import "../lifecycle/Pausable.sol";

interface ITokenList {
function isAllowed(address) external returns (bool);
function maxAmount(address) external returns (uint256);
function minAmount(address) external returns (uint256);
}

interface IWrappedCoin {
function deposit() external payable;
}

contract TokenCashierV3 is Pausable {
event Receipt(address indexed token, uint256 indexed id, address sender, address recipient, uint256 amount, uint256 fee, bytes payload);
// event ContractDestinationAdded(address indexed destination);
// event ContractDestinationRemoved(address indexed destination);

ITokenList[] public tokenLists;
address[] public tokenSafes;
mapping(address => uint256) public counts;
uint256 public depositFee;
IWrappedCoin public wrappedCoin;
// mapping(address => bool) public contractDestinations;

constructor(IWrappedCoin _wrappedCoin, ITokenList[] memory _tokenLists, address[] memory _tokenSafes) public {
require(_tokenLists.length == _tokenSafes.length, "# of token lists is not equal to # of safes");
wrappedCoin = _wrappedCoin;
tokenLists = _tokenLists;
tokenSafes = _tokenSafes;
}

// receive() external {
// revert();
// }

function count(address _token) public view returns (uint256) {
return counts[_token];
}

function setDepositFee(uint256 _fee) public onlyOwner {
depositFee = _fee;
}

function depositTo(address _token, address _to, uint256 _amount, bytes memory _payload) public whenNotPaused payable {
require(_to != address(0), "invalid destination");
bool isCoin = false;
uint256 fee = msg.value;
if (_token == address(0)) {
require(msg.value >= _amount, "insufficient msg.value");
fee = msg.value - _amount;
wrappedCoin.deposit.value(_amount)();
_token = address(wrappedCoin);
isCoin = true;
}
require(fee >= depositFee, "insufficient fee");
for (uint256 i = 0; i < tokenLists.length; i++) {
if (tokenLists[i].isAllowed(_token)) {
require(_amount >= tokenLists[i].minAmount(_token), "amount too low");
require(_amount <= tokenLists[i].maxAmount(_token), "amount too high");
if (tokenSafes[i] == address(0)) {
require(!isCoin && safeTransferFrom(_token, msg.sender, address(this), _amount), "fail to transfer token to cashier");
// selector = bytes4(keccak256(bytes('burn(uint256)')))
(bool success, bytes memory data) = _token.call(abi.encodeWithSelector(0x42966c68, _amount));
require(success && (data.length == 0 || abi.decode(data, (bool))), "fail to burn token");
} else {
if (isCoin) {
require(safeTransfer(_token, tokenSafes[i], _amount), "failed to put into safe");
} else {
require(safeTransferFrom(_token, msg.sender, tokenSafes[i], _amount), "failed to put into safe");
}
}
counts[_token] += 1;
emit Receipt(_token, counts[_token], msg.sender, _to, _amount, fee, _payload);
return;
}
}
revert("not a whitelisted token");
}

function deposit(address _token, uint256 _amount, bytes memory _payload) public payable {
depositTo(_token, msg.sender, _amount, _payload);
}

function withdraw() external onlyOwner {
msg.sender.transfer(address(this).balance);
}

function withdrawToken(address _token) public onlyOwner {
// selector = bytes4(keccak256(bytes('balanceOf(address)')))
(bool success, bytes memory balance) = _token.call(abi.encodeWithSelector(0x70a08231, address(this)));
require(success, "failed to call balanceOf");
uint256 bal = abi.decode(balance, (uint256));
if (bal > 0) {
require(safeTransfer(_token, msg.sender, bal), "failed to withdraw token");
}
}

function safeTransferFrom(address _token, address _from, address _to, uint256 _amount) internal returns (bool) {
// selector = bytes4(keccak256(bytes('transferFrom(address,address,uint256)')))
(bool success, bytes memory data) = _token.call(abi.encodeWithSelector(0x23b872dd, _from, _to, _amount));
return success && (data.length == 0 || abi.decode(data, (bool)));
}

function safeTransfer(address _token, address _to, uint256 _amount) internal returns (bool) {
// selector = bytes4(keccak256(bytes('transfer(address,uint256)')))
(bool success, bytes memory data) = _token.call(abi.encodeWithSelector(0xa9059cbb, _to, _amount));
return success && (data.length == 0 || abi.decode(data, (bool)));
}
}
135 changes: 135 additions & 0 deletions contracts/iotube/TransferValidatorV3.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
pragma solidity <6.0 >=0.4.24;

import "../lifecycle/Pausable.sol";

interface IAllowlist {
function isAllowed(address) external view returns (bool);
function numOfActive() external view returns (uint256);
}

interface IMinter {
function mint(address, address, uint256) external returns(bool);
function transferOwnership(address) external;
function owner() external view returns(address);
}

interface IReceiver {
function onReceive(address sender, address token, uint256 amount, bytes calldata payload) external;
}

contract TransferValidatorV3 is Pausable {
event Settled(bytes32 indexed key, address[] witnesses);
event ReceiverAdded(address receiver);
event ReceiverRemoved(address receiver);

mapping(bytes32 => uint256) public settles;
mapping(address => bool) public receivers;

IMinter[] public minters;
IAllowlist[] public tokenLists;
IAllowlist public witnessList;

constructor(IAllowlist _witnessList) public {
witnessList = _witnessList;
}

function generateKey(address cashier, address tokenAddr, uint256 index, address from, address to, uint256 amount, bytes memory payload) public view returns(bytes32) {
return keccak256(abi.encodePacked(address(this), cashier, tokenAddr, index, from, to, amount, payload));
}

function submit(address cashier, address tokenAddr, uint256 index, address from, address to, uint256 amount, bytes memory signatures, bytes memory payload) public whenNotPaused {
require(amount != 0, "amount cannot be zero");
require(to != address(0), "recipient cannot be zero");
require(signatures.length % 65 == 0, "invalid signature length");
bytes32 key = generateKey(cashier, tokenAddr, index, from, to, amount, payload);
require(settles[key] == 0, "transfer has been settled");
for (uint256 it = 0; it < tokenLists.length; it++) {
if (tokenLists[it].isAllowed(tokenAddr)) {
uint256 numOfSignatures = signatures.length / 65;
address[] memory witnesses = new address[](numOfSignatures);
for (uint256 i = 0; i < numOfSignatures; i++) {
address witness = recover(key, signatures, i * 65);
require(witnessList.isAllowed(witness), "invalid signature");
for (uint256 j = 0; j < i; j++) {
require(witness != witnesses[j], "duplicate witness");
}
witnesses[i] = witness;
}
require(numOfSignatures * 3 > witnessList.numOfActive() * 2, "insufficient witnesses");
settles[key] = block.number;
require(minters[it].mint(tokenAddr, to, amount), "failed to mint token");
if (receivers[to]) {
IReceiver(to).onReceive(from, tokenAddr, amount, payload);
}
emit Settled(key, witnesses);
return;
}
}
revert("not a whitelisted token");
}

function numOfPairs() external view returns (uint256) {
return tokenLists.length;
}

function addPair(IAllowlist _tokenList, IMinter _minter) external onlyOwner {
tokenLists.push(_tokenList);
minters.push(_minter);
}

function addReceiver(address _receiver) external onlyOwner {
require(!receivers[_receiver], "already a receiver");
receivers[_receiver] = true;
emit ReceiverAdded(_receiver);
}

function removeReceiver(address _receiver) external onlyOwner {
require(receivers[_receiver], "invalid receiver");
receivers[_receiver] = false;
emit ReceiverRemoved(_receiver);
}

function upgrade(address _newValidator) external onlyOwner {
address contractAddr = address(this);
for (uint256 i = 0; i < minters.length; i++) {
IMinter minter = minters[i];
if (minter.owner() == contractAddr) {
minter.transferOwnership(_newValidator);
}
}
}

/**
* @dev Recover signer address from a message by using their signature
* @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address.
* @param signature bytes signature, the signature is generated using web3.eth.sign()
*/
function recover(bytes32 hash, bytes memory signature, uint256 offset)
internal
pure
returns (address)
{
bytes32 r;
bytes32 s;
uint8 v;

// Divide the signature in r, s and v variables with inline assembly.
assembly {
r := mload(add(signature, add(offset, 0x20)))
s := mload(add(signature, add(offset, 0x40)))
v := byte(0, mload(add(signature, add(offset, 0x60))))
}

// Version of signature should be 27 or 28, but 0 and 1 are also possible versions
if (v < 27) {
v += 27;
}

// If the version is correct return the signer address
if (v != 27 && v != 28) {
return (address(0));
}
// solium-disable-next-line arg-overflow
return ecrecover(hash, v, r, s);
}
}
34 changes: 34 additions & 0 deletions contracts/iotube/Unwrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: MIT
pragma solidity >= 0.8.0;

import "../ownership/Ownable.sol";

interface ICrosschainToken {
function withdrawTo(address, uint256) external;
}

contract Unwrapper is Ownable {
mapping(address => bool) private whitelist;

constructor() Ownable() {

}

function onReceive(address _sender, ICrosschainToken _token, uint256 _amount, bytes calldata _payload) external {
require(whitelist[msg.sender], "invalid caller");
address recipient = _sender;
if (_payload.length == 32) {
(recipient) = abi.decode(_payload, (address));
}
_token.withdrawTo(recipient, _amount);
}

function addWhitelist(address _addr) external onlyOwner {
whitelist[_addr] = true;
}

function removeWhitelist(address _addr) external onlyOwner {
whitelist[_addr] = false;
}
}

2 changes: 1 addition & 1 deletion contracts/ownership/Ownable.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pragma solidity ^0.5.0;
pragma solidity >=0.5.0;


/**
Expand Down
20 changes: 10 additions & 10 deletions scripts/alter_transfer_table.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.bsc_transfers ADD txSender varchar(42);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.ethereum_transfers ADD txSender varchar(42);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.heco_transfers ADD txSender varchar(42);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.matic_transfers ADD txSender varchar(42);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.polis_transfers ADD txSender varchar(42);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_transfers ADD txSender varchar(42);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_heco_transfers ADD txSender varchar(42);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_polis_transfers ADD txSender varchar(42);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_bsc_transfers ADD txSender varchar(42);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_matic_transfers ADD txSender varchar(42);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.bsc_transfers ADD payload varchar(24576);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.ethereum_transfers ADD payload varchar(24576);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.heco_transfers ADD payload varchar(24576);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.matic_transfers ADD payload varchar(24576);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.polis_transfers ADD payload varchar(24576);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_transfers ADD payload varchar(24576);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_heco_transfers ADD payload varchar(24576);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_polis_transfers ADD payload varchar(24576);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_bsc_transfers ADD payload varchar(24576);"
docker exec witness-db mysql -uroot -pkdfjjrU64fjK58H -e "ALTER TABLE witness.iotex_to_matic_transfers ADD payload varchar(24576);"
68 changes: 68 additions & 0 deletions test/TransferValidatorV3.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const TransferValidator = artifacts.require('TransferValidatorV3');
const Unwrapper = artifacts.require('Unwrapper');
const MinterPool = artifacts.require('MinterPool');
const ShadowToken = artifacts.require('ShadowToken');
const TokenList = artifacts.require('TokenList');
const WitnessList = artifacts.require('WitnessList');
const CrosschainERC20 = artifacts.require('CrosschainERC20');
const StandardToken = artifacts.require('StandardToken');
const Account = require('eth-lib/lib/account');
const {AbiCoder} = require('ethers');

const witnessPrivateKeys = [
'0x388c684f0ba1ef5017716adb5d21a053ea8e90277d0868337519f97bede61418',
'0x659cbb0e2411a44db63778987b1e22153c086a95eb6b18bdf89de078917abc63',
'0x82d052c865f5763aad42add438569276c00d3d88a2d062d36b2bae914d58b8c8',
'0xaa3680d5d48a8283413f7a108367c7299ca73f553735860a87b08f39395618b7',
'0x0f62d96d6675f32685bbdb8ac13cda7c23436f63efbb9d07700d8669ff12b7c4',
];

contract('TransferValidatorV3', function([owner, minter, sender, relayer, witness1, witness2, witness3, witness4, cashier, receiver]) {
beforeEach(async function() {
this.witnessList = await WitnessList.new();
this.validator = await TransferValidator.new(this.witnessList.address);
this.unwrapper = await Unwrapper.new();
this.minterPool = await MinterPool.new();
this.mintableToken = await ShadowToken.new(this.minterPool.address, "0x0000000000000000000000000000000000000000", "token to mint", "mt", 18, {from: minter});
this.mintableTokenList = await TokenList.new();
await this.mintableTokenList.addToken(this.mintableToken.address, 1, 100000);
await this.validator.addPair(this.mintableTokenList.address, this.minterPool.address);
await this.unwrapper.addWhitelist(this.validator.address);
await this.validator.addReceiver(this.unwrapper.address);
await this.witnessList.addWitness(witness1);
});
it('is not reciever', async function() {
await this.minterPool.transferOwnership(this.validator.address);
const payload = "0x";
const key = await this.validator.generateKey(cashier, this.mintableToken.address, 321, sender, receiver, 12345, payload);
const signature = await Account.sign(key, witnessPrivateKeys[0]);
assert.equal(await this.validator.settles(key), 0);
const tx = await this.validator.submit(cashier, this.mintableToken.address, 321, sender, receiver, 12345, signature, payload);
assert.notEqual(await this.validator.settles(key), 0);
assert.equal(await this.mintableToken.balanceOf(receiver), 12345);
assert.equal(tx.logs.length, 1);
assert.equal(tx.logs[0].event, "Settled");
assert.equal(tx.logs[0].args.key, key);
assert.equal(tx.logs[0].args.witnesses.length, 1);
assert.equal(tx.logs[0].args.witnesses[0], witness1);
})
it('is reciever', async function() {
this.crosschainERC20 = await CrosschainERC20.new(this.mintableToken.address, this.minterPool.address, "token to mint", "cerc20", 18);
await this.minterPool.mint(this.mintableToken.address, this.crosschainERC20.address, 30000);
await this.minterPool.transferOwnership(this.validator.address);
await this.mintableTokenList.addToken(this.crosschainERC20.address, 1, 100000);
const payload = AbiCoder.defaultAbiCoder().encode(["address"], [receiver]);
const key = await this.validator.generateKey(cashier, this.crosschainERC20.address, 321, sender, this.unwrapper.address, 12345, payload);
const signature = await Account.sign(key, witnessPrivateKeys[0]);
assert.equal(await this.validator.settles(key), 0);
const tx = await this.validator.submit(cashier, this.crosschainERC20.address, 321, sender, this.unwrapper.address, 12345, signature, payload);
assert.notEqual(await this.validator.settles(key), 0);
assert.equal(await this.mintableToken.balanceOf(this.unwrapper.address), 0);
assert.equal(await this.mintableToken.balanceOf(receiver), 12345);
assert.equal(tx.logs.length, 1);
assert.equal(tx.logs[0].event, "Settled");
assert.equal(tx.logs[0].args.key, key);
assert.equal(tx.logs[0].args.witnesses.length, 1);
assert.equal(tx.logs[0].args.witnesses[0], witness1);
})
})
Loading