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

Forced Transfer #253

Merged
merged 6 commits into from
Sep 16, 2018
Merged
Show file tree
Hide file tree
Changes from 2 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
75 changes: 75 additions & 0 deletions contracts/tokens/SecurityToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ contract SecurityToken is StandardToken, DetailedERC20, ReentrancyGuard, Registr
// Use to permanently halt all minting
bool public mintingFrozen;

// addresses whitelisted by issuer as controller
mapping (address => bool) public isController;
mapping (address => uint256) private controllerID;
address[] public controllers;

struct ModuleData {
bytes32 name;
address moduleAddress;
Expand Down Expand Up @@ -110,6 +115,10 @@ contract SecurityToken is StandardToken, DetailedERC20, ReentrancyGuard, Registr
event Minted(address indexed to, uint256 amount);
event Burnt(address indexed _burner, uint256 _value);

// Events to log controller actions
event LogChangeControllerStatus(address indexed _controller, bool _status);
event LogControllerTransfer(address indexed _controller, address indexed _from, address indexed _to, uint256 _amount, bytes _data);

// Require msg.sender to be the specified module type
modifier onlyModule(uint8 _moduleType) {
bool isModuleType = false;
Expand Down Expand Up @@ -145,6 +154,14 @@ contract SecurityToken is StandardToken, DetailedERC20, ReentrancyGuard, Registr
_;
}

/**
* @notice Revert if called by account which is not a controller
*/
modifier onlyController() {
require(isController[msg.sender]);
_;
}

/**
* @notice Constructor
* @param _name Name of the SecurityToken
Expand Down Expand Up @@ -721,4 +738,62 @@ contract SecurityToken is StandardToken, DetailedERC20, ReentrancyGuard, Registr
return _getValueAt(checkpointBalances[_investor], _checkpointId, balanceOf(_investor));
}

/**
* @notice Use to get the list of controller addresses
* @return address array containing the list of controller addresses
*/
function getControllers() public view returns (address[]) {
return controllers;
}

/**
* @notice Use by the issuer ot set the status of a controller addresses
* @param _controller address of the controller
* @param _status bool representing new status to set
*/
function setControllerStatus(address _controller, bool _status) public onlyOwner {
require(isController[_controller] != _status);
if (_status == true) {
isController[_controller] = true;
controllerID[_controller] = controllers.length;
controllers.push(_controller);
} else {
isController[_controller] = false;
uint256 i = controllerID[_controller];
address last = controllers[controllers.length - 1];
controllers[i] = last;
controllerID[last] = i;

delete controllers[controllers.length - 1];
delete controllerID[_controller];
controllers.length--;
}
emit LogChangeControllerStatus(_controller, _status);
}

/**
* @notice Use by a controller to execute a foced transfer
* @param _from address from which to take tokens
* @param _to address where to send tokens
* @param _value amount of tokens to transfer
* @param _data data attached to the transfer by controller for event
*/
function controllerTransfer(address _from, address _to, uint256 _value, bytes _data) public onlyController {
Copy link
Contributor

@pabloruiz55 pabloruiz55 Sep 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's _data for?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a reason string explaining why the transfer occured which gets logged as an event

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this function also need to be added to the constructor?
transferFunctions[bytes4(keccak256(...))] = true;
@adamdossa

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only if it calls verifyTransfer()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern can be revisited using internal / private functions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - if it is going to call verifyTransfer then it should do so that TMs know they should record any side-effects of the transfer.

One option here is to call verifyTransfer but disregard the result (or just include the result in the ControlledTransfer event).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of adding the result to the event

require(_value <= balances[_from]);
require(_to != address(0));

_adjustInvestorCount(_from, _to, _value);
_adjustBalanceCheckpoints(_from);
_adjustBalanceCheckpoints(_to);

// Consider passing setting to record change in state but not transfer check (could be a part of standard messaging module)
// require(verifyTransfer(_from, _to, _value), "Transfer is not valid");

balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);

emit LogControllerTransfer(msg.sender, _from, _to, _value, _data);
emit Transfer(_from, _to, _value);
pabloruiz55 marked this conversation as resolved.
Show resolved Hide resolved
}

}
183 changes: 178 additions & 5 deletions test/o_security_token.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ contract('SecurityToken', accounts => {
let account_fundsReceiver;
let account_delegate;
let account_temp;
let account_controller;

let balanceOfReceiver;
// investor Details
Expand Down Expand Up @@ -123,16 +124,17 @@ contract('SecurityToken', accounts => {
// Accounts setup
account_polymath = accounts[0];
account_issuer = accounts[1];
account_investor1 = accounts[9];
account_investor2 = accounts[6];
account_investor3 = accounts[7];
account_affiliate1 = accounts[2];
account_affiliate2 = accounts[3];
account_fundsReceiver = accounts[4];
account_delegate = accounts[5];
account_investor2 = accounts[6];
account_investor3 = accounts[7];
account_temp = accounts[8];
account_affiliate1 = accounts[2];
account_affiliate2 = accounts[3];
account_investor1 = accounts[9];

token_owner = account_issuer;
account_controller = account_temp;

// ----------- POLYMATH NETWORK Configuration ------------

Expand Down Expand Up @@ -1268,4 +1270,175 @@ contract('SecurityToken', accounts => {
});
});

describe("Force Transfer", async() => {

it("Should fail to set controller status because msg.sender not owner", async() => {
let errorThrown = false;
try {
await I_SecurityToken.setControllerStatus(account_controller, true, {from: account_controller});
} catch (error) {
console.log(` tx revert -> msg.sender not owner`.grey);
errorThrown = true;
ensureException(error);
}
assert.ok(errorThrown, message);
});

it("Should fail to set controller status because status unchanged", async() => {
let errorThrown = false;
try {
await I_SecurityToken.setControllerStatus(account_controller, false, {from: token_owner});
} catch (error) {
console.log(` tx revert -> status unchanged`.grey);
errorThrown = true;
ensureException(error);
}
assert.ok(errorThrown, message);
});

it("Should successfully set controller status to true", async() => {
let tx1 = await I_SecurityToken.setControllerStatus(account_investor1, true, {from: token_owner});
let tx2 = await I_SecurityToken.setControllerStatus(account_controller, true, {from: token_owner});
let tx3 = await I_SecurityToken.setControllerStatus(account_investor2, true, {from: token_owner});

// check event
assert.equal(account_investor1, tx1.logs[0].args._controller, "Event not emitted as expected");
assert.equal(true, tx1.logs[0].args._status, "Event not emitted as expected");
assert.equal(account_controller, tx2.logs[0].args._controller, "Event not emitted as expected");
assert.equal(true, tx2.logs[0].args._status, "Event not emitted as expected");
assert.equal(account_investor2, tx3.logs[0].args._controller, "Event not emitted as expected");
assert.equal(true, tx3.logs[0].args._status, "Event not emitted as expected");

// check status
let status1 = await I_SecurityToken.isController(account_investor1);
let status2 = await I_SecurityToken.isController(account_controller);
let status3 = await I_SecurityToken.isController(account_investor2);
assert.equal(true, status1, "Status not set correctly");
assert.equal(true, status2, "Status not set correctly");
assert.equal(true, status3, "Status not set correctly");

// check array
let controllers = await I_SecurityToken.getControllers();
assert.equal(account_investor1, controllers[0], "Array of controllers not correctly set");
assert.equal(account_controller, controllers[1], "Array of controllers not correctly set");
assert.equal(account_investor2, controllers[controllers.length - 1], "Array of controllers not correctly set");
});

it("Should successfully set controller status to false", async() => {
let tx1 = await I_SecurityToken.setControllerStatus(account_investor1, false, {from: token_owner});

// check event
assert.equal(account_investor1, tx1.logs[0].args._controller, "Event not emitted as expected");
assert.equal(false, tx1.logs[0].args._status, "Event not emitted as expected");

// check status
let status1 = await I_SecurityToken.isController(account_investor1);
let status2 = await I_SecurityToken.isController(account_controller);
let status3 = await I_SecurityToken.isController(account_investor2);
assert.equal(false, status1, "Status not set correctly");
assert.equal(true, status2, "Status not set correctly");
assert.equal(true, status3, "Status not set correctly");

// check array
let controllers = await I_SecurityToken.getControllers();
assert.equal(account_investor2, controllers[0], "Array of controllers not correctly set");
assert.equal(account_controller, controllers[controllers.length - 1], "Array of controllers not correctly set");

let tx2 = await I_SecurityToken.setControllerStatus(account_investor2, false, {from: token_owner});

// check event
assert.equal(account_investor2, tx2.logs[0].args._controller, "Event not emitted as expected");
assert.equal(false, tx2.logs[0].args._status, "Event not emitted as expected");

// check status
status1 = await I_SecurityToken.isController(account_investor1);
status2 = await I_SecurityToken.isController(account_controller);
status3 = await I_SecurityToken.isController(account_investor2);
assert.equal(false, status1, "Status not set correctly");
assert.equal(true, status2, "Status not set correctly");
assert.equal(false, status3, "Status not set correctly");

// check array
controllers = await I_SecurityToken.getControllers();
assert.equal(account_controller, controllers[0], "Array of controllers not correctly set");
assert.equal(account_controller, controllers[controllers.length - 1], "Array of controllers not correctly set");
});

it("Should fail to controllerTransfer because not approved controller", async() => {
let errorThrown1 = false;
try {
await I_SecurityToken.controllerTransfer(account_investor1, account_investor2, web3.utils.toWei("10", "ether"), "reason", {from: account_investor1});
} catch (error) {
console.log(` tx revert -> not approved controller`.grey);
errorThrown1 = true;
ensureException(error);
}
assert.ok(errorThrown1, message);

let errorThrown2 = false;
try {
await I_SecurityToken.controllerTransfer(account_investor1, account_investor2, web3.utils.toWei("10", "ether"), "reason", {from: account_investor2});
} catch (error) {
console.log(` tx revert -> not approved controller`.grey);
errorThrown2 = true;
ensureException(error);
}
assert.ok(errorThrown2, message);
});

it("Should fail to controllerTransfer because insufficient balance", async() => {
let errorThrown = false;
try {
await I_SecurityToken.controllerTransfer(account_investor2, account_investor1, web3.utils.toWei("10", "ether"), "reason", {from: account_controller});
} catch (error) {
console.log(` tx revert -> insufficient balance`.grey);
errorThrown = true;
ensureException(error);
}
assert.ok(errorThrown, message);
});

it("Should fail to controllerTransfer because recipient is zero address", async() => {
let errorThrown = false;
try {
await I_SecurityToken.controllerTransfer(account_investor1, '0x0000000000000000000000000000000000000000', web3.utils.toWei("10", "ether"), "reason", {from: account_controller});
} catch (error) {
console.log(` tx revert -> recipient is zero address`.grey);
errorThrown = true;
ensureException(error);
}
assert.ok(errorThrown, message);
});

it("Should successfully controllerTransfer", async() => {
let sender = account_investor1;
let receiver = account_investor2;

let start_investorCount = await I_SecurityToken.investorCount.call();
let start_balInv1 = await I_SecurityToken.balanceOf.call(account_investor1);
let start_balInv2 = await I_SecurityToken.balanceOf.call(account_investor2);

let tx = await I_SecurityToken.controllerTransfer(account_investor1, account_investor2, web3.utils.toWei("10", "ether"), "reason", {from: account_controller});

let end_investorCount = await I_SecurityToken.investorCount.call();
let end_balInv1 = await I_SecurityToken.balanceOf.call(account_investor1);
let end_balInv2 = await I_SecurityToken.balanceOf.call(account_investor2);

assert.equal(start_investorCount.add(1).toNumber(), end_investorCount.toNumber(), "Investor count not changed");
assert.equal(start_balInv1.sub(web3.utils.toWei("10", "ether")).toNumber(), end_balInv1.toNumber(), "Investor balance not changed");
assert.equal(start_balInv2.add(web3.utils.toWei("10", "ether")).toNumber(), end_balInv2.toNumber(), "Investor balance not changed");

assert.equal(account_controller, tx.logs[0].args._controller, "Event not emitted as expected");
assert.equal(account_investor1, tx.logs[0].args._from, "Event not emitted as expected");
assert.equal(account_investor2, tx.logs[0].args._to, "Event not emitted as expected");
assert.equal(web3.utils.toWei("10", "ether"), tx.logs[0].args._amount, "Event not emitted as expected");
assert.equal("reason", web3.utils.hexToUtf8(tx.logs[0].args._data), "Event not emitted as expected");

assert.equal(account_investor1, tx.logs[1].args.from, "Event not emitted as expected");
assert.equal(account_investor2, tx.logs[1].args.to, "Event not emitted as expected");
assert.equal(web3.utils.toWei("10", "ether"), tx.logs[1].args.value, "Event not emitted as expected");
});

});

});