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 acknowledgement for irrevocable actions using EIP712 #599

Merged
merged 13 commits into from
Mar 22, 2019
92 changes: 92 additions & 0 deletions contracts/libraries/TokenLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ library TokenLib {

using SafeMath for uint256;

struct EIP712Domain {
string name;
satyamakgec marked this conversation as resolved.
Show resolved Hide resolved
uint256 chainId;
address verifyingContract;
}

struct Acknowledgment {
string text;
}

bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,uint256 chainId,address verifyingContract)"
);

bytes32 constant ACK_TYPEHASH = keccak256(
"Acknowledgment(string text)"
);

bytes32 internal constant WHITELIST = "WHITELIST";
bytes32 internal constant INVESTORSKEY = 0xdf3a8dd24acdd05addfc6aeffef7574d2de3f844535ec91e8e0f3e45dba96731; //keccak256(abi.encodePacked("INVESTORS"))

Expand All @@ -26,6 +44,80 @@ library TokenLib {
// Emit when the budget allocated to a module is changed
event ModuleBudgetChanged(uint8[] _moduleTypes, address _module, uint256 _oldBudget, uint256 _budget);

function hash(EIP712Domain memory _eip712Domain) internal pure returns (bytes32) {
return keccak256(
abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256(bytes(_eip712Domain.name)),
_eip712Domain.chainId,
_eip712Domain.verifyingContract
)
);
}

function hash(Acknowledgment memory _ack) internal pure returns (bytes32) {
return keccak256(abi.encode(ACK_TYPEHASH, keccak256(bytes(_ack.text))));
}

function recoverFreezeIssuanceAckSigner(bytes memory _signature) public view returns (address) {
Acknowledgment memory ack = Acknowledgment("I acknowledge that freezing Issuance is a permanent and irrevocable change");
return extractSigner(ack, _signature);
}

function recoverDisableControllerAckSigner(bytes memory _signature) public view returns (address) {
Acknowledgment memory ack = Acknowledgment("I acknowledge that disabling controller is a permanent and irrevocable change");
return extractSigner(ack, _signature);
}

function extractSigner(Acknowledgment memory _ack, bytes memory _signature) internal view returns (address) {
bytes32 r;
bytes32 s;
uint8 v;

// Check the signature length
if (_signature.length != 65) {
return (address(0));
}

// Divide the signature in r, s and v variables
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
// solhint-disable-next-line no-inline-assembly
assembly {
r := mload(add(_signature, 0x20))
s := mload(add(_signature, 0x40))
v := byte(0, mload(add(_signature, 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));
}

bytes32 DOMAIN_SEPARATOR = hash(
EIP712Domain(
{
name: "Polymath",
chainId: 1,
verifyingContract: address(this)
}
)
);

// Note: we need to use `encodePacked` here instead of `encode`.
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hash(_ack)
));
return ecrecover(digest, v, r, s);
}

/**
* @notice Archives a module attached to the SecurityToken
* @param _moduleData Storage data
Expand Down
15 changes: 5 additions & 10 deletions contracts/tokens/SecurityToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import "../interfaces/token/IERC1594.sol";
import "../interfaces/token/IERC1643.sol";
import "../interfaces/token/IERC1644.sol";
import "../interfaces/IModuleRegistry.sol";
import "../interfaces/IFeatureRegistry.sol";
import "../interfaces/ITransferManager.sol";
import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "openzeppelin-solidity/contracts/utils/ReentrancyGuard.sol";
Expand Down Expand Up @@ -115,11 +114,6 @@ contract SecurityToken is ERC20, ERC20Detailed, Ownable, ReentrancyGuard, Securi
_;
}

modifier isEnabled(string memory _nameKey) {
require(IFeatureRegistry(featureRegistry).getFeatureStatus(_nameKey));
_;
}

/**
* @notice constructor
* @param _name Name of the SecurityToken
Expand Down Expand Up @@ -485,7 +479,8 @@ contract SecurityToken is ERC20, ERC20Detailed, Ownable, ReentrancyGuard, Securi
* @notice Permanently freeze issuance of this security token.
* @dev It MUST NOT be possible to increase `totalSuppy` after this function is called.
*/
function freezeIssuance() external isIssuanceAllowed isEnabled("freezeIssuanceAllowed") onlyOwner {
function freezeIssuance(bytes calldata _signature) external isIssuanceAllowed onlyOwner {
require(owner() == TokenLib.recoverFreezeIssuanceAckSigner(_signature), "Owner did not sign");
satyamakgec marked this conversation as resolved.
Show resolved Hide resolved
issuance = false;
/*solium-disable-next-line security/no-block-members*/
emit FreezeIssuance();
Expand Down Expand Up @@ -614,8 +609,9 @@ contract SecurityToken is ERC20, ERC20Detailed, Ownable, ReentrancyGuard, Securi
* @notice Used by the issuer to permanently disable controller functionality
* @dev enabled via feature switch "disableControllerAllowed"
*/
function disableController() external isEnabled("disableControllerAllowed") onlyOwner {
require(isControllable());
function disableController(bytes calldata _signature) external onlyOwner {
require(owner() == TokenLib.recoverDisableControllerAckSigner(_signature), "Owner did not sign");
require(_isControllable());
controllerDisabled = true;
delete controller;
emit DisableController();
Expand Down Expand Up @@ -768,7 +764,6 @@ contract SecurityToken is ERC20, ERC20Detailed, Ownable, ReentrancyGuard, Securi
function updateFromRegistry() public onlyOwner {
moduleRegistry = PolymathRegistry(polymathRegistry).getAddress("ModuleRegistry");
securityTokenRegistry = PolymathRegistry(polymathRegistry).getAddress("SecurityTokenRegistry");
featureRegistry = PolymathRegistry(polymathRegistry).getAddress("FeatureRegistry");
polyToken = PolymathRegistry(polymathRegistry).getAddress("PolyToken");
}
}
1 change: 0 additions & 1 deletion contracts/tokens/SecurityTokenStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ contract SecurityTokenStorage {
address public polymathRegistry;
address public moduleRegistry;
address public securityTokenRegistry;
address public featureRegistry;
address public polyToken;
address public delegate;
// Address of the data store used to store shared data
Expand Down
89 changes: 88 additions & 1 deletion test/helpers/signData.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const Web3 = require("web3");
//const sigUtil = require('eth-sig-util')
let BN = Web3.utils.BN;

function getSignSTMData(tmAddress, nonce, validFrom, expiry, fromAddress, toAddress, amount, pk) {
Expand Down Expand Up @@ -32,6 +33,90 @@ function getSignSTMData(tmAddress, nonce, validFrom, expiry, fromAddress, toAddr
return data;
}

async function getFreezeIssuanceAck(stAddress, from) {
const typedData = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' }
],
Acknowledgment: [
{ name: 'text', type: 'string' }
],
},
primaryType: 'Acknowledgment',
domain: {
name: 'Polymath',
chainId: 1,
verifyingContract: stAddress
},
message: {
text: 'I acknowledge that freezing Issuance is a permanent and irrevocable change',
},
};
const result = await new Promise((resolve, reject) => {
web3.currentProvider.send(
{
method: 'eth_signTypedData',
params: [from, typedData]
},
(err, result) => {
if (err) {
return reject(err);
}
resolve(result.result);
}
);
});
// console.log('signed by', from);
// const recovered = sigUtil.recoverTypedSignature({
// data: typedData,
// sig: result
// })
// console.log('recovered address', recovered);
return result;
}

async function getDisableControllerAck(stAddress, from) {
const typedData = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' }
],
Acknowledgment: [
{ name: 'text', type: 'string' }
],
},
primaryType: 'Acknowledgment',
domain: {
name: 'Polymath',
chainId: 1,
verifyingContract: stAddress
},
message: {
text: 'I acknowledge that disabling controller is a permanent and irrevocable change',
},
};
const result = await new Promise((resolve, reject) => {
web3.currentProvider.send(
{
method: 'eth_signTypedData',
params: [from, typedData]
},
(err, result) => {
if (err) {
return reject(err);
}
resolve(result.result);
}
);
});
return result;
}

function getSignGTMData(tmAddress, investorAddress, fromTime, toTime, expiryTime, validFrom, validTo, nonce, pk) {
let hash = web3.utils.soliditySha3({
t: 'address',
Expand Down Expand Up @@ -109,5 +194,7 @@ module.exports = {
getSignSTMData,
getSignGTMData,
getSignGTMTransferData,
getMultiSignGTMData
getMultiSignGTMData,
getFreezeIssuanceAck,
getDisableControllerAck
};
Loading