Last active
October 14, 2018 03:18
-
-
Save cwhinfrey/1f8854b6d332041eba94a72b410805e9 to your computer and use it in GitHub Desktop.
SignatureBouncer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
pragma solidity ^0.4.24; | |
library ECDSA { | |
/** | |
* @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 signature) | |
internal | |
pure | |
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. | |
// solium-disable-next-line security/no-inline-assembly | |
assembly { | |
r := mload(add(signature, 32)) | |
s := mload(add(signature, 64)) | |
v := byte(0, mload(add(signature, 96))) | |
} | |
// 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)); | |
} else { | |
// solium-disable-next-line arg-overflow | |
return ecrecover(hash, v, r, s); | |
} | |
} | |
/** | |
* toEthSignedMessageHash | |
* @dev prefix a bytes32 value with "\x19Ethereum Signed Message:" | |
* and hash the result | |
*/ | |
function toEthSignedMessageHash(bytes32 hash) | |
internal | |
pure | |
returns (bytes32) | |
{ | |
// 32 is the length in bytes of hash, | |
// enforced by the type signature above | |
return keccak256( | |
abi.encodePacked("\x19Ethereum Signed Message:\n32", hash) | |
); | |
} | |
} | |
library Roles { | |
struct Role { | |
mapping (address => bool) bearer; | |
} | |
/** | |
* @dev give an account access to this role | |
*/ | |
function add(Role storage role, address account) internal { | |
require(account != address(0)); | |
role.bearer[account] = true; | |
} | |
/** | |
* @dev remove an account's access to this role | |
*/ | |
function remove(Role storage role, address account) internal { | |
require(account != address(0)); | |
role.bearer[account] = false; | |
} | |
/** | |
* @dev check if an account has this role | |
* @return bool | |
*/ | |
function has(Role storage role, address account) | |
internal | |
view | |
returns (bool) | |
{ | |
require(account != address(0)); | |
return role.bearer[account]; | |
} | |
} | |
contract SignerRole { | |
using Roles for Roles.Role; | |
event SignerAdded(address indexed account); | |
event SignerRemoved(address indexed account); | |
Roles.Role private signers; | |
constructor() public { | |
signers.add(msg.sender); | |
} | |
modifier onlySigner() { | |
require(isSigner(msg.sender)); | |
_; | |
} | |
function isSigner(address account) public view returns (bool) { | |
return signers.has(account); | |
} | |
function addSigner(address account) public onlySigner { | |
signers.add(account); | |
emit SignerAdded(account); | |
} | |
function renounceSigner() public { | |
signers.remove(msg.sender); | |
} | |
function _removeSigner(address account) internal { | |
signers.remove(account); | |
emit SignerRemoved(account); | |
} | |
} | |
contract SignatureBouncer is SignerRole { | |
event Log(bytes a, bytes32 b); | |
using ECDSA for bytes32; | |
// Function selectors are 4 bytes long, as documented in | |
// https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector | |
uint256 private constant _METHOD_ID_SIZE = 4; | |
// Signature size is 65 bytes (tightly packed v + r + s), but gets padded to 96 bytes | |
uint256 private constant _SIGNATURE_SIZE = 96; | |
/** | |
* @dev requires that a valid signature of a signer was provided | |
*/ | |
modifier onlyValidSignature(bytes signature, bytes32 id) | |
{ | |
require(_replayGuard(id)); | |
require(_isValidSignature(msg.sender, signature)); | |
_; | |
} | |
/** | |
* @dev requires that a valid signature with a specifed method of a signer was provided | |
*/ | |
modifier onlyValidSignatureAndMethod(bytes signature, bytes32 id) | |
{ | |
require(_replayGuard(id)); | |
require(_isValidSignatureAndMethod(msg.sender, signature)); | |
_; | |
} | |
/** | |
* @dev requires that a valid signature with a specifed method and params of a signer was provided | |
*/ | |
modifier onlyValidSignatureAndData(bytes signature, bytes32 id) | |
{ | |
require(_replayGuard(id)); | |
require(_isValidSignatureAndData(msg.sender, signature)); | |
_; | |
} | |
/** | |
* @dev is the signature of `this + sender` from a signer? | |
* @return bool | |
*/ | |
function _isValidSignature(address account, bytes signature) | |
internal | |
view | |
returns (bool) | |
{ | |
return _isValidDataHash( | |
keccak256(abi.encodePacked(address(this), account)), | |
signature | |
); | |
} | |
/** | |
* @dev is the signature of `this + sender + methodId` from a signer? | |
* @return bool | |
*/ | |
function _isValidSignatureAndMethod(address account, bytes signature) | |
internal | |
view | |
returns (bool) | |
{ | |
bytes memory data = new bytes(_METHOD_ID_SIZE); | |
for (uint i = 0; i < data.length; i++) { | |
data[i] = msg.data[i]; | |
} | |
return _isValidDataHash( | |
keccak256(abi.encodePacked(address(this), account, data)), | |
signature | |
); | |
} | |
/** | |
* @dev is the signature of `this + sender + methodId + params(s)` from a signer? | |
* @notice the signature parameter of the method being validated must be the "last" parameter | |
* @return bool | |
*/ | |
function _isValidSignatureAndData(address account, bytes signature) | |
internal | |
view | |
returns (bool) | |
{ | |
require(msg.data.length > _SIGNATURE_SIZE); | |
bytes memory data = new bytes(msg.data.length - _SIGNATURE_SIZE); | |
for (uint i = 0; i < data.length; i++) { | |
data[i] = msg.data[i]; | |
} | |
return _isValidDataHash( | |
keccak256(abi.encodePacked(address(this), account, data)), | |
signature | |
); | |
} | |
/** | |
* @dev internal function to convert a hash to an eth signed message | |
* and then recover the signature and check it against the signer role | |
* @return bool | |
*/ | |
function _isValidDataHash(bytes32 hash, bytes signature) | |
internal | |
view | |
returns (bool) | |
{ | |
address signer = hash | |
.toEthSignedMessageHash() | |
.recover(signature); | |
return signer != address(0) && isSigner(signer); | |
} | |
function _replayGuard(bytes32 id) internal returns (bool); | |
} | |
contract OneTransactionPerUserTest is SignatureBouncer { | |
uint256 public foo; | |
mapping (address => bool) public hasSetFoo; | |
function setFoo(uint256 val, bytes sig) public onlyValidSignature(sig, 0) { | |
foo = val; | |
} | |
// Use this to get the hash rather than computing it in javascript | |
function getHash(address account) public view returns (bytes32) { | |
return keccak256(abi.encodePacked(address(this), account)); | |
} | |
function _replayGuard(bytes32 /*id*/) internal returns (bool) { | |
// Check if this address has already set foo | |
require(!hasSetFoo[msg.sender]); | |
hasSetFoo[msg.sender] = true; | |
return true; | |
} | |
} | |
contract UserNonceTest is SignatureBouncer { | |
uint256 public foo; | |
mapping (address => uint256) nextNonce; | |
function setFoo(uint256 val, uint256 nonce, bytes sig) public onlyValidSignatureAndData(sig, bytes32(nonce)) { | |
foo = val; | |
} | |
// Use this to get the hash rather than computing it in javascript | |
function getHash(uint256 val, uint256 nonce) public view returns (bytes32) { | |
bytes32 a = 0x60; | |
bytes32 messageLength = bytes32(65); | |
return keccak256( | |
abi.encodePacked( | |
address(this), | |
msg.sender, | |
this.setFoo.selector, | |
val, | |
nonce, | |
a, | |
messageLength | |
) | |
); | |
} | |
function _replayGuard(bytes32 id) internal returns (bool) { | |
// Check if this address has already used nonce | |
require(nextNonce[msg.sender] == uint256(id)); | |
nextNonce[msg.sender]++; | |
return true; | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment