// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;
/*
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with GSN meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address payable) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes memory) {
this;
// silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691
return msg.data;
}
}
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* By default, the owner account will be the one that deploys the contract. This
* can later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor () {
address msgSender = _msgSender();
_owner = msgSender;
emit OwnershipTransferred(address(0), msgSender);
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view returns (address) {
return _owner;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(_owner == _msgSender(), "Ownable: caller is not the owner");
_;
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions anymore. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby removing any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
}
/**
* @dev contract module that provides the OpenCerts DocumentStore contract to be used
* to issue and remoke certificates on the blockchain
*/
contract DocumentStore is Ownable {
string public name;
string public version = "2.3.0";
/// A mapping of the document hash to the block number that was issued
mapping(bytes32 => uint256) public documentIssued;
/// A mapping of the hash of the claim being revoked to the revocation block number
mapping(bytes32 => uint256) public documentRevoked;
event DocumentIssued(bytes32 indexed document);
event DocumentRevoked(bytes32 indexed document);
constructor(string memory _name) {
name = _name;
}
function issue(bytes32 document) public onlyOwner onlyNotIssued(document) {
documentIssued[document] = block.number;
emit DocumentIssued(document);
}
function bulkIssue(bytes32[] memory documents) public {
for (uint256 i = 0; i < documents.length; i++) {
issue(documents[i]);
}
}
function getIssuedBlock(bytes32 document)
public
view
onlyIssued(document)
returns (uint256)
{
return documentIssued[document];
}
function isIssued(bytes32 document) public view returns (bool) {
return (documentIssued[document] != 0);
}
function isIssuedBefore(bytes32 document, uint256 blockNumber)
public
view
returns (bool)
{
return
documentIssued[document] != 0 && documentIssued[document] <= blockNumber;
}
function revoke(bytes32 document)
public
onlyOwner
onlyNotRevoked(document)
{
documentRevoked[document] = block.number;
emit DocumentRevoked(document);
}
function bulkRevoke(bytes32[] memory documents) public {
for (uint256 i = 0; i < documents.length; i++) {
revoke(documents[i]);
}
}
function isRevoked(bytes32 document) public view returns (bool) {
return documentRevoked[document] != 0;
}
function isRevokedBefore(bytes32 document, uint256 blockNumber)
public
view
returns (bool)
{
return
documentRevoked[document] <= blockNumber && documentRevoked[document] != 0;
}
modifier onlyIssued(bytes32 document) {
require(
isIssued(document),
"Error: Only issued document hashes can be revoked"
);
_;
}
modifier onlyNotIssued(bytes32 document) {
require(
!isIssued(document),
"Error: Only hashes that have not been issued can be issued"
);
_;
}
modifier onlyNotRevoked(bytes32 claim) {
require(!isRevoked(claim), "Error: Hash has been revoked previously");
_;
}
}
/**
*
* AccredifyMultiSig
* ============
*
* Basic multi-signer wallet designed for use in a co-signing environment where
* 2 signatures are required to issue and revoke certificates.
* Typically used in a 2-of-3 signing configuration. Uses ecrecover to allow
* for 2 signatures in a single transaction.
*
* The first signature is created on the operation hash (see Data Formats) and
* passed to the multiSig functions
* The signer is determined by verifyMultiSig().
*
* The second signature is created by the submitter of the transaction and determined
* by msg.signer.
*
* This wallet only allows for three signers that have to be set upon deployment.
* The signers cannot be changed/removed/added.
*
* This iteration of the wallet restricts the initiation of the different functions to
* specific signers, and removes the expiryTime param, does not deploy a docStore contract,
* and removes events.
*
* Signer Authorisation
* ====================
* All multisig transactions can only be initiated by the 3rd Signer AKA The Custodian
*
* Data Formats
* ============
*
* The signature is created with ethereumjs-util.ecsign(operationHash).
* Like the eth_sign RPC call, it packs the values as a 65-byte array of [r, s, v].
* Unlike eth_sign, the message is not prefixed.
*
* The operationHash the result of keccak256(prefix, hash).
* For Issue transactions, `prefix` is "ISSUE".
* For Bulk Issue transactions, `prefix` is "BULKISSUE".
* For Revoke transaction, `prefix` is "REVOKE".
* For Bulk Revoke transaction, `prefix` is "BULKREVOKE".
* For Transfer transaction, 'prefix' is "TRANSFER"
* For Change transaction, 'prefix' is "CHANGE"
*
*/
contract AccredifyMultiSigDocStore {
// Public fields
address[] public signers; // The addresses that can co-sign transactions on the wallet
DocumentStore public documentStore; //DocumentStore Contract
/**
* Constructor
* ============
*
* Deploys a new AccredifyMultiSig contract
* Takes in an array of 3 signers that will be used to approve transactions
*
*/
constructor(address[] memory _signers) {
require(_signers.length == 3, "3 signers required");
signers = _signers;
}
/**
* Modifier that will execute internal code block only if the sender is the Custodian
*/
modifier onlyCustodian {
require(msg.sender == signers[2], "Wrong initiator");
_;
}
/**
* Fallback function. Is called when a transaction is received without calling a method
*/
fallback() external {
revert();
// Reject any accidental Ether transfer
}
/**
* Execute a multi-signature issue transaction from this wallet using 2 signers: one from msg.sender and the other from ecrecover.
*
* @param hash the certificate batch's hash that will be appended to the blockchain
* @param signature second signer's signature
*/
function issue(
bytes32 hash,
bytes memory signature
) public onlyCustodian {
bytes32 operationHash = keccak256(abi.encodePacked("ISSUE", hash));
verify(operationHash, signature, 0);
documentStore.issue(hash);
}
/**
* Execute a multi-signature bulk issue transaction from this wallet using 2 signers: one from msg.sender and the other from ecrecover.
*
* @param hashes an array of certificate batch hashes that will be appended to the blockchain
* @param signature second signer's signature
*/
function bulkIssue(
bytes32[] memory hashes,
bytes memory signature
) public onlyCustodian {
bytes32 operationHash = keccak256(abi.encodePacked("BULKISSUE", hashes));
verify(operationHash, signature, 0);
documentStore.bulkIssue(hashes);
}
/**
* Execute a multi-signature revoke transaction from this wallet using 2 signers: one from msg.sender and the other from ecrecover.
*
* @param hash the certificate's hash that will be revoked on the blockchain
* @param signature second signer's signature
*/
function revoke(
bytes32 hash,
bytes memory signature
) public onlyCustodian {
bytes32 operationHash = keccak256(abi.encodePacked("REVOKE", hash));
verify(operationHash, signature, 0);
documentStore.revoke(hash);
}
/**
* Execute a multi-signature bulk revoke transaction from this wallet using 2 signers: one from msg.sender and the other from ecrecover.
*
* @param hashes an array of certificate hashes that will be revoked on the blockchain
* @param signature second signer's signature
*/
function bulkRevoke(
bytes32[] memory hashes,
bytes memory signature
) public onlyCustodian {
bytes32 operationHash = keccak256(abi.encodePacked("BULKREVOKE", hashes));
verify(operationHash, signature, 0);
documentStore.bulkRevoke(hashes);
}
/**
* Execute a multi-signature transfer transaction from this wallet using 2 signers: one from msg.sender and the other from ecrecover.
* This transaction transfers the ownership of the certificate store to a new owner.
*
* @param newOwner the new owner's address
* @param signature second signer's signature
*/
function transfer(
address newOwner,
bytes memory signature
) public onlyCustodian {
bytes32 operationHash = keccak256(abi.encodePacked("TRANSFER", newOwner));
verify(operationHash, signature, 1);
documentStore.transferOwnership(newOwner);
}
/**
* Execute a multi-signature change transaction from this wallet using 2 signers: one from msg.sender and the other from ecrecover.
* This transaction changes the address of the DocumentStore on this wallet contract.
*
* @param newStore the new owner's address
* @param signature second signer's signature
*/
function changeStore(
address newStore,
bytes memory signature
) public onlyCustodian {
bytes32 operationHash = keccak256(abi.encodePacked("CHANGE", newStore));
verify(operationHash, signature, 1);
documentStore = DocumentStore(newStore);
}
/**
* Do common multisig verification for both Issue and Revoke transactions
*
* @param operationHash keccak256(prefix, hash)
* @param signature second signer's signature
* @param signer order in singers array
* returns address that has created the signature
*/
function verify(
bytes32 operationHash,
bytes memory signature,
uint8 signer
) private view {
address otherSigner = recoverAddress(operationHash, signature);
require(otherSigner == signers[signer], "Wrong signer");
}
/**
* Gets signer's address using ecrecover
* @param operationHash see Data Formats
* @param signature see Data Formats
* returns address recovered from the signature
*/
function recoverAddress(
bytes32 operationHash,
bytes memory signature
) private pure returns (address) {
if (signature.length != 65) {
revert();
}
// We need to unpack the signature, which is given as an array of 65 bytes (like eth.sign)
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := and(mload(add(signature, 65)), 255)
}
if (v < 27) {
v += 27;
// Ethereum versions are 27 or 28 as opposed to 0 or 1 which is submitted by some signing libs
}
return ecrecover(operationHash, v, r, s);
}
}
{
"compilationTarget": {
"browser/AccredifyMultiSig.sol": "AccredifyMultiSigDocStore"
},
"evmVersion": "istanbul",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 200
},
"remappings": []
}
[{"inputs":[{"internalType":"address[]","name":"_signers","type":"address[]"}],"stateMutability":"nonpayable","type":"constructor"},{"stateMutability":"nonpayable","type":"fallback"},{"inputs":[{"internalType":"bytes32[]","name":"hashes","type":"bytes32[]"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"bulkIssue","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32[]","name":"hashes","type":"bytes32[]"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"bulkRevoke","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newStore","type":"address"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"changeStore","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"documentStore","outputs":[{"internalType":"contract DocumentStore","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"issue","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"revoke","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"signers","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"transfer","outputs":[],"stateMutability":"nonpayable","type":"function"}]