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

Agent app: EVM #580

Merged
merged 45 commits into from
Feb 15, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
0d01aaf
Actor app: inherit Vault and test vault functionality
izqui Nov 15, 2018
1fa6386
Actor app: implement execute action
izqui Nov 16, 2018
7a4dd5b
Actor app: implement permissioned forwarding
izqui Nov 16, 2018
9830981
Actor app: improve tests
izqui Nov 16, 2018
7457eb3
Actor app: signatures
izqui Nov 19, 2018
af478c1
CI: travis wait on npm install
izqui Nov 19, 2018
ebb10aa
Actor app: lint and enable CI
izqui Nov 19, 2018
e132297
Add max gas for static calls
izqui Nov 26, 2018
3ad9920
chore: upgrade to [email protected]
sohkai Jan 26, 2019
ea9a5aa
Address review comments
izqui Jan 26, 2019
8e40798
Lint
izqui Feb 4, 2019
2aaf462
test: add comments, cosmetic changes, and more tests
sohkai Feb 7, 2019
c73d999
SignatureValidator: fix compile warnings
sohkai Feb 7, 2019
5f1291f
test: add context headers
sohkai Feb 7, 2019
f17a0f5
Metadata: add roles to arapp, remove webapp artifacts
izqui Feb 11, 2019
f6f66f7
Radspec strings
izqui Feb 11, 2019
eb95db8
Travis: allow actor coverage to fail, tracking fix in #658
izqui Feb 11, 2019
abf1c91
Prevent infinite loop by prohibiting setting itself as the designated…
izqui Feb 11, 2019
a10c62c
Radspec
izqui Feb 11, 2019
cc21b67
Actor -> Agent rename
izqui Feb 11, 2019
139ad8a
Add status to arapp and rename constants
izqui Feb 12, 2019
fe86092
Update ERC1271 implementation
izqui Feb 12, 2019
1849286
Add ERC1271 interfaceId
izqui Feb 12, 2019
1b7b735
Rename constants
izqui Feb 12, 2019
d960fee
Rename calldata
izqui Feb 12, 2019
39b6b7e
Refactor SignatureValidator.sol
izqui Feb 12, 2019
de9d4a6
Refactor signature validation tests and test EIP712 signatures
izqui Feb 12, 2019
474f0c1
Handle empty signatures and undefined signature modes
izqui Feb 12, 2019
345a9d8
Refactor SignatureValidator: handle ERC1271 checks in library
izqui Feb 13, 2019
499cdcb
Test ERC1271 signature wrapping
izqui Feb 13, 2019
1b76fb9
cosmetic: fix whitespace
sohkai Feb 13, 2019
98369d1
Merge branch 'next' into actor-app
sohkai Feb 13, 2019
a0bddef
Agent: always allow execute to transfer ETH (#651)
sohkai Feb 13, 2019
e8eb268
cosmetic: fix whitespace and add EIPs hash
sohkai Feb 13, 2019
2225408
chore: upgrade aragonOS and use test-helper's contracts
sohkai Feb 13, 2019
2ed4a2a
cosmetic: fix compile errors
sohkai Feb 13, 2019
c482981
cosmetic: fix linter
sohkai Feb 13, 2019
cd9614b
chore: fix travis allowed to fail matrix
sohkai Feb 13, 2019
970431a
test: ignore all test contracts from coverage
sohkai Feb 13, 2019
cff0689
Actor: reuse Vault tests (#668)
sohkai Feb 14, 2019
905c32d
Move agent from future-apps/ to apps/
izqui Feb 15, 2019
0d3e2ec
Refactor popFirstByte to copy to a new array
izqui Feb 15, 2019
5b7a5fe
Address last review comments
izqui Feb 15, 2019
2a293d1
Lint
izqui Feb 15, 2019
687e29d
Address very last review comments
izqui Feb 15, 2019
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
58 changes: 3 additions & 55 deletions future-apps/agent/contracts/Agent.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ contract Agent is IERC165, ERC1271Bytes, IForwarder, IsContract, Vault {
string private constant ERROR_EXECUTE_TARGET_NOT_CONTRACT = "AGENT_EXEC_TARGET_NO_CONTRACT";
string private constant ERROR_DESIGNATED_TO_SELF = "AGENT_DESIGNATED_TO_SELF";

uint256 internal constant ISVALIDSIG_MAX_GAS = 250000;
uint256 internal constant EIP165_MAX_GAS = 30000;

mapping (bytes32 => bool) public isPresigned;
address public designatedSigner;

Expand Down Expand Up @@ -130,22 +127,11 @@ contract Agent is IERC165, ERC1271Bytes, IForwarder, IsContract, Vault {
// Short-circuit in case the hash was presigned. Optimization as performing calls
// and ecrecover is more expensive than an SLOAD.
if (isPresigned[hash]) {
return isValidSignatureReturn(true);
}

// Checks if designatedSigner is a contract, and if it supports the isValidSignature interface
if (safeSupportsInterface(IERC165(designatedSigner), ERC1271_INTERFACE_ID)) {
// designatedSigner.isValidSignature(hash, signature) as a staticall
ERC1271 signerContract = ERC1271(designatedSigner);
bytes memory data = abi.encodeWithSelector(signerContract.isValidSignature.selector, hash, signature);
return isValidSignatureReturn(safeBoolStaticCall(signerContract, data, ISVALIDSIG_MAX_GAS));
return returnIsValidSignatureMagicNumber(true);
}

// `safeSupportsInterface` returns false if designatedSigner is a contract but it
// doesn't support the interface. Here we check the validity of the ECDSA sig
// which will always fail if designatedSigner is not an EOA

return isValidSignatureReturn(SignatureValidator.isValidSignature(hash, designatedSigner, signature));
bool isValid = SignatureValidator.isValidSignature(hash, designatedSigner, signature);
return returnIsValidSignatureMagicNumber(isValid);
}

function canForward(address sender, bytes evmScript) public view returns (bool) {
Expand All @@ -154,44 +140,6 @@ contract Agent is IERC165, ERC1271Bytes, IForwarder, IsContract, Vault {
return canPerform(sender, RUN_SCRIPT_ROLE, params);
}

function safeSupportsInterface(IERC165 target, bytes4 interfaceId) internal view returns (bool) {
if (!isContract(target)) {
return false;
}

bytes memory data = abi.encodeWithSelector(target.supportsInterface.selector, interfaceId);
return safeBoolStaticCall(target, data, EIP165_MAX_GAS);
}

function safeBoolStaticCall(address target, bytes data, uint256 maxGas) internal view returns (bool) {
uint256 gasLeft = gasleft();

uint256 callGas = gasLeft > maxGas ? maxGas : gasLeft;
bool ok;
assembly {
ok := staticcall(callGas, target, add(data, 0x20), mload(data), 0, 0)
}

if (!ok) {
return false;
}

uint256 size;
assembly { size := returndatasize }
if (size != 32) {
return false;
}

bool result;
assembly {
let ptr := mload(0x40) // get next free memory ptr
returndatacopy(ptr, 0, size) // copy return from above `staticcall`
result := mload(ptr) // read data at ptr and set it to result
}

return result;
}

function getScriptACLParam(bytes evmScript) internal pure returns (uint256) {
return uint256(keccak256(abi.encodePacked(evmScript)));
}
Expand Down
120 changes: 99 additions & 21 deletions future-apps/agent/contracts/SignatureValidator.sol
Original file line number Diff line number Diff line change
@@ -1,36 +1,74 @@
pragma solidity 0.4.24;

// From github.com/DexyProject/protocol
// + https://github.com/0xProject/0x-monorepo/blob/master/packages/contracts/contracts/protocol/Exchange/MixinSignatureValidator.sol
// Inspired in https://github.com/horizon-games/multi-token-standard/blob/master/contracts/utils/SignatureValidator.sol
// This should probably be moved into aOS: https://github.com/aragon/aragonOS/pull/442

import "./standards/ERC1271.sol";


library SignatureValidator {
enum SignatureMode {
Invalid,
EIP712,
EthSign
Invalid, // 0x00
EIP712, // 0x01
EthSign, // 0x02
ERC1271, // 0x03
NMode // 0x04, to check if mode is specified, leave at the end
}

// bytes4(keccak256("isValidSignature(bytes,bytes)")
bytes4 constant public ERC1271_RETURN_VALID_SIGNATURE = 0x20c13b0b;
uint256 internal constant ERC1271_ISVALIDSIG_MAX_GAS = 250000;

/// @dev Validates that a hash was signed by a specified signer.
/// @param hash Hash which was signed.
/// @param signer Address of the signer.
/// @param signature ECDSA signature along with the mode (0 = Invalid, 1 = EIP712, 2 = EthSign) {mode}{r}{s}{v}.
/// @param signature ECDSA signature along with the mode (0 = Invalid, 1 = EIP712, 2 = EthSign, 3 = ERC1271) {mode}{r}{s}{v}.
/// @return Returns whether signature is from a specified user.
function isValidSignature(bytes32 hash, address signer, bytes signature) internal pure returns (bool) {
function isValidSignature(bytes32 hash, address signer, bytes signature) internal view returns (bool) {
if (signature.length == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be <= 1 since mode is always required

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Length being 1 is valid for ERC1271 mode signatures. We could perform the check before sending it over to the erc1271 contract, but that contract may have logic which allows empty signatures

return false;
}

SignatureMode mode = SignatureMode(uint8(signature[0]));
uint8 modeByte = uint8(signature[0]);
if (modeByte >= uint8(SignatureMode.NMode)) {
return false;
}
SignatureMode mode = SignatureMode(modeByte);

if (mode == SignatureMode.Invalid || signature.length != 66) {
if (mode == SignatureMode.EIP712) {
return ecVerify(hash, signer, signature);
} else if (mode == SignatureMode.EthSign) {
return ecVerify(
keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)),
signer,
signature
);
} else if (mode == SignatureMode.ERC1271) {
// Pop the mode byte before sending it down the validation chain
izqui marked this conversation as resolved.
Show resolved Hide resolved
(bytes memory newSig,) = popFirstByte(signature); // `signature` IS CORRUPTED AFTER EXECUTING THIS
return safeIsValidSignature(signer, hash, signature);
} else {
return false;
}
}

function ecVerify(bytes32 hash, address signer, bytes memory signature) internal pure returns (bool) {
(bool badSig, bytes32 r, bytes32 s, uint8 v) = unpackEcSig(signature);

uint8 v = uint8(signature[65]);
bytes32 r;
bytes32 s;
if (badSig) {
return false;
}

return signer == ecrecover(hash, v, r, s);
}

function unpackEcSig(bytes memory signature) internal pure returns (bool badSig, bytes32 r, bytes32 s, uint8 v) {
if (signature.length != 66) {
badSig = true;
return;
}

v = uint8(signature[65]);
assembly {
r := mload(add(signature, 33))
s := mload(add(signature, 65))
Expand All @@ -42,18 +80,58 @@ library SignatureValidator {
}

if (v != 27 && v != 28) {
return false;
badSig = true;
}
}

bytes32 signedHash;
if (mode == SignatureMode.EthSign) {
signedHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
} else if (mode == SignatureMode.EIP712) {
signedHash = hash;
} else {
return false;
/**
* @dev DANGEROUS FUNCTION: Modifies input in place, making it invalid after using this function
* Memory layout: bytes in parenthesis are returned
* L = l - 1
* Input: ([l1][l2]....[l32][b1][b2]....[bn])
* Output: [00]([L1][L2]....[L32][b2]....[bn])
*/
function popFirstByte(bytes memory input) internal pure returns (bytes memory output, byte firstByte) {
assembly {
let length := mload(input)
firstByte := mload(add(input, 0x20))
output := add(input, 0x01)
izqui marked this conversation as resolved.
Show resolved Hide resolved
mstore8(input, 0x00) // Probably safe to remove, as it will most already likely be 0
sohkai marked this conversation as resolved.
Show resolved Hide resolved
mstore(output, sub(length, 0x01))
}
}

function safeIsValidSignature(address validator, bytes32 hash, bytes memory signature) internal view returns (bool) {
bytes memory data = abi.encodeWithSelector(ERC1271(validator).isValidSignature.selector, hash, signature);
bytes4 erc1271Return = safeBytes4StaticCall(validator, data, ERC1271_ISVALIDSIG_MAX_GAS);
return erc1271Return == ERC1271_RETURN_VALID_SIGNATURE;
}

function safeBytes4StaticCall(address target, bytes data, uint256 maxGas) internal view returns (bytes4 ret) {
uint256 gasLeft = gasleft();

uint256 callGas = gasLeft > maxGas ? maxGas : gasLeft;
bool ok;
assembly {
ok := staticcall(callGas, target, add(data, 0x20), mload(data), 0, 0)
}

if (!ok) {
return;
}

uint256 size;
assembly { size := returndatasize }
if (size != 32) {
return;
}

assembly {
let ptr := mload(0x40) // get next free memory ptr
returndatacopy(ptr, 0, size) // copy return from above `staticcall`
ret := mload(ptr) // read data at ptr and set it to be returned
}

return ecrecover(signedHash, v, r, s) == signer;
return ret;
}
}
2 changes: 1 addition & 1 deletion future-apps/agent/contracts/standards/ERC1271.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ contract ERC1271 {
*/
function isValidSignature(bytes32 _hash, bytes memory _signature) public view returns (bytes4);

function isValidSignatureReturn(bool isValid) internal pure returns (bytes4) {
function returnIsValidSignatureMagicNumber(bool isValid) internal pure returns (bytes4) {
return isValid ? ERC1271_RETURN_VALID_SIGNATURE : ERC1271_RETURN_INVALID_SIGNATURE;
}
}
Expand Down
8 changes: 6 additions & 2 deletions future-apps/agent/contracts/test/mocks/DesignatedSigner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ contract DesignatedSigner /* is IERC165, ERC1271 */ {
bool isValidRevert;
bool modifyState;

// bytes4(keccak256("isValidSignature(bytes,bytes)")
bytes4 constant public ERC1271_RETURN_VALID_SIGNATURE = 0x20c13b0b; // TODO: Likely needs to be updated
bytes4 constant public ERC1271_RETURN_INVALID_SIGNATURE = 0x00000000;

constructor (bool _isInterface, bool _isValid, bool _isValidRevert, bool _modifyState) public {
isInterface = _isInterface;
isValid = _isValid;
Expand All @@ -23,13 +27,13 @@ contract DesignatedSigner /* is IERC165, ERC1271 */ {
}

// Can't be ERC1271-compliant since this potentially modifies state
function isValidSignature(bytes32, bytes) external returns (bool) {
function isValidSignature(bytes32, bytes) external returns (bytes4) {
require(!isValidRevert);

if (modifyState) {
modifyState = false;
}

return isValid;
return isValid ? ERC1271_RETURN_VALID_SIGNATURE : ERC1271_RETURN_INVALID_SIGNATURE;
}
}
42 changes: 30 additions & 12 deletions future-apps/agent/test/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,14 @@ contract('Agent app', (accounts) => {
const [_, nobody, presigner, signerDesignator] = accounts
const HASH = web3.sha3('hash') // careful as it may encode the data in the same way as solidity before hashing

const SIGNATURE_MODES = {
Invalid: '0x00',
EIP712: '0x01',
EthSign: '0x02',
ERC1271: '0x03',
NMode: '0x04',
}

const ERC1271_RETURN_VALID_SIGNATURE = '0x20c13b0b'
const ERC1271_RETURN_INVALID_SIGNATURE = '0x00000000'

Expand Down Expand Up @@ -314,24 +322,22 @@ contract('Agent app', (accounts) => {
const signatureTests = [
{
name: 'EIP712',
mode: 1,
signFunction: eip712Sign,
signer: '0x93070b307c373D7f9344859E909e3EEeF6E4Fd5a',
signerOrKey: '11bc31e7fef59610dfd6f95d2f78d2396c7b5477e4a9a54d72d9c1b76930e5c1',
notSignerOrKey: '7224b5bc510e01f75b10e3b6d6c903861ca91adb95a26406d1603e2d28a29e7f',
},
{
name: 'EthSign',
mode: 2,
signFunction: ethSign,
signer: accounts[7],
signerOrKey: accounts[7],
notSignerOrKey: accounts[8]
}
]

for (let { name, mode, signFunction, signer, signerOrKey, notSignerOrKey } of signatureTests) {
const sign = signFunctionGenerator(signFunction, mode)
for (let { name, signFunction, signer, signerOrKey, notSignerOrKey } of signatureTests) {
const sign = signFunctionGenerator(signFunction, SIGNATURE_MODES[name])

context(`> Signature mode: ${name}`, () => {
beforeEach(async () => {
Expand Down Expand Up @@ -365,15 +371,25 @@ contract('Agent app', (accounts) => {
})
}

context(`> Signature mode: Invalid`, () => {
context(`> Signature mode: invalid modes`, () => {
const randomAccount = accounts[9]

beforeEach(async () => {
await agent.setDesignatedSigner(randomAccount, { from: signerDesignator })
})

it('isValidSignature returns false to an invalid mode signature', async () => {
const invalidSignature = SIGNATURE_MODES.Invalid
assertIsValidSignature(false, await agent.isValidSignature(HASH, invalidSignature))
})

it('isValidSignature returns false to an unspecified mode signature', async () => {
const unspecifiedSignature = SIGNATURE_MODES.NMode
assertIsValidSignature(false, await agent.isValidSignature(HASH, unspecifiedSignature))
})

it('isValidSignature returns true to an invalid signature iff the hash was presigned', async () => {
const invalidSignature = "0x00"
const invalidSignature = SIGNATURE_MODES.Invalid
assertIsValidSignature(false, await agent.isValidSignature(HASH, invalidSignature))

// Now presign it
Expand All @@ -384,6 +400,8 @@ contract('Agent app', (accounts) => {
})

context('> Designated signer: contracts', () => {
const ERC1271_SIG = SIGNATURE_MODES.ERC1271

const setDesignatedSignerContract = async (...params) => {
const designatedSigner = await DesignatedSigner.new(...params)
return agent.setDesignatedSigner(designatedSigner.address, { from: signerDesignator })
Expand All @@ -396,7 +414,7 @@ contract('Agent app', (accounts) => {
// false - doesn't modify state on checking sig
await setDesignatedSignerContract(true, true, false, false)

assertIsValidSignature(true, await agent.isValidSignature(HASH, NO_SIG))
assertIsValidSignature(true, await agent.isValidSignature(HASH, ERC1271_SIG))
})

it('isValidSignature returns false if designated signer returns false', async () => {
Expand All @@ -407,18 +425,18 @@ contract('Agent app', (accounts) => {
await setDesignatedSignerContract(true, false, false, false)

// Signature fails check
assertIsValidSignature(false, await agent.isValidSignature(HASH, NO_SIG))
assertIsValidSignature(false, await agent.isValidSignature(HASH, ERC1271_SIG))
})

it('isValidSignature returns false if designated signer doesnt support the interface', async () => {
it('isValidSignature returns true even if the designated signer doesnt support the interface', async () => {
// false - not ERC165 interface compliant
// true - any sigs are valid
// false - doesn't revert on checking sig
// false - doesn't modify state on checking sig
await setDesignatedSignerContract(false, true, false, false)

// Requires ERC165 compliance before checking isValidSignature
izqui marked this conversation as resolved.
Show resolved Hide resolved
assertIsValidSignature(false, await agent.isValidSignature(HASH, NO_SIG))
assertIsValidSignature(true, await agent.isValidSignature(HASH, ERC1271_SIG))
})

it('isValidSignature returns false if designated signer reverts', async () => {
Expand All @@ -429,7 +447,7 @@ contract('Agent app', (accounts) => {
await setDesignatedSignerContract(true, true, true, false)

// Reverts on checking
assertIsValidSignature(false, await agent.isValidSignature(HASH, NO_SIG))
assertIsValidSignature(false, await agent.isValidSignature(HASH, ERC1271_SIG))
})

it('isValidSignature returns false if designated signer attempts to modify state', async () => {
Expand All @@ -440,7 +458,7 @@ contract('Agent app', (accounts) => {
await setDesignatedSignerContract(true, true, false, true)

// Checking costs too much gas
assertIsValidSignature(false, await agent.isValidSignature(HASH, NO_SIG))
assertIsValidSignature(false, await agent.isValidSignature(HASH, ERC1271_SIG))
})
})
})
Expand Down