Skip to content

Instantly share code, notes, and snippets.

@cwhinfrey
Last active October 14, 2018 03:18
Show Gist options
  • Save cwhinfrey/1f8854b6d332041eba94a72b410805e9 to your computer and use it in GitHub Desktop.
Save cwhinfrey/1f8854b6d332041eba94a72b410805e9 to your computer and use it in GitHub Desktop.
SignatureBouncer
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