Skip to content

Commit 942968d

Browse files
committed
Change r, s, v arrays to bytes
1 parent 355c413 commit 942968d

12 files changed

+107
-112
lines changed

README.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ There are multiple implementations of the Gnosis Safe contract with different me
5454
#### GnosisSafePersonalEdition.sol
5555
This version is targeted at users that control all keys owning a safe. The transaction hash can be signed with the private keys that manage the safe.
5656

57-
Once the required number of confirmations is available `execAndPayTransaction` can be called with the sending confirmation signatures. This method will pay the submitter of the transaction for the transaction fees after the Safe transaction has been executed.
57+
Once the required number of confirmations is available `execTransactionAndPaySubmitter` can be called with the sending confirmation signatures. This method will pay the submitter of the transaction for the transaction fees after the Safe transaction has been executed.
5858

59-
`execAndPayTransaction` expects all confirmations sorted by owner address. This is required to easily validate no confirmation duplicates exist.
59+
`execTransactionAndPaySubmitter` expects all confirmations sorted by owner address. This is required to easily validate no confirmation duplicates exist.
6060

6161
#### GnosisSafeTeamEdition.sol
6262
This version is targeted at teams where each owner is a different user. Each owner has to confirm a transaction by using `confirmTransaction`. Once the required number of owners has confirmed, the transaction can be executed via `execTransactionIfApproved`. If the sender of `execTransactionIfApproved` is an owner it is not necessary to confirm the transaction before. Furthermore this version doesn't store the nonce in the contract but for each transaction a nonce needs to be specified.
@@ -70,12 +70,10 @@ Assuming we have 2 owners in a 2 out of 2 multisig configuration:
7070

7171
`0x1` and `0x2` are confirming by signing a message.
7272

73-
The Safe transaction parameters used for `execTransaction` have to be set like the following:
74-
* `v = [v_0x1, v_0x2]`
75-
* `r = [r_0x1, r_0x2]`
76-
* `s = [s_0x1, s_0x2]`
73+
The signatures bytes used for `execTransaction` have to be build like the following:
74+
* `bytes = 0x{r_0x1}{s_0x1}{v_0x1}{r_0x2}{s_0x2}{v_0x2}`
7775

78-
`v`, `r` and `s` are the signature parameters for the signed confirmation messages. Position `0` in `v` represents `0x1`'s signature part and corresponds to position `0` in `r` and `s`.
76+
`v`, `r` and `s` are the signature parameters for the signed confirmation messages. All values are hex encoded. `r` and `s` are padded to 32 bytes and `v` is padded to 8 bytes.
7977

8078
### Modules
8179
Modules allow to execute transactions from the Safe without the requirement of multiple signatures. For this Modules that have been added to a Safe can use the `execTransactionFromModule` function. Modules define their own requirements for execution. Modules need to implement their own replay protection.

contracts/GnosisSafePersonalEdition.sol

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ pragma solidity 0.4.24;
22
import "./interfaces/ERC20Token.sol";
33
import "./GnosisSafe.sol";
44
import "./MasterCopy.sol";
5+
import "./SignatureValidator.sol";
56

67

78
/// @title Gnosis Safe Personal Edition - A multisignature wallet with support for confirmations using signed messages based on ERC191.
89
/// @author Stefan George - <stefan@gnosis.pm>
910
/// @author Richard Meissner - <richard@gnosis.pm>
1011
/// @author Ricardo Guilherme Schmidt - (Status Research & Development GmbH) - Gas Token Payment
11-
contract GnosisSafePersonalEdition is MasterCopy, GnosisSafe {
12+
contract GnosisSafePersonalEdition is MasterCopy, GnosisSafe, SignatureValidator {
1213

1314
string public constant NAME = "Gnosis Safe Personal Edition";
1415
string public constant VERSION = "0.0.1";
@@ -17,7 +18,8 @@ contract GnosisSafePersonalEdition is MasterCopy, GnosisSafe {
1718

1819
uint256 public nonce;
1920

20-
/// @dev Allows to execute a Safe transaction confirmed by required number of owners.
21+
/// @dev Allows to execute a Safe transaction confirmed by required number of owners and then pays the account that submitted the transaction.
22+
/// Note: The fees are always transfered, even if the user transaction fails.
2123
/// @param to Destination address of Safe transaction.
2224
/// @param value Ether value of Safe transaction.
2325
/// @param data Data payload of Safe transaction.
@@ -26,10 +28,8 @@ contract GnosisSafePersonalEdition is MasterCopy, GnosisSafe {
2628
/// @param dataGas Gas costs for data used to trigger the safe transaction and to pay the payment transfer
2729
/// @param gasPrice Gas price that should be used for the payment calculation.
2830
/// @param gasToken Token address (or 0 if ETH) that is used for the payment.
29-
/// @param v Array of signature V values sorted by owner addresses.
30-
/// @param r Array of signature R values sorted by owner addresses.
31-
/// @param s Array of signature S values sorted by owner addresses.
32-
function execAndPayTransaction(
31+
/// @param signatures Packed signature data ({bytes32 r}{bytes32 s}{uint8 v})
32+
function execTransactionAndPaySubmitter(
3333
address to,
3434
uint256 value,
3535
bytes data,
@@ -38,19 +38,19 @@ contract GnosisSafePersonalEdition is MasterCopy, GnosisSafe {
3838
uint256 dataGas,
3939
uint256 gasPrice,
4040
address gasToken,
41-
uint8[] v,
42-
bytes32[] r,
43-
bytes32[] s
41+
bytes signatures
4442
)
4543
public
44+
returns (bool success)
4645
{
4746
uint256 startGas = gasleft();
4847
bytes32 txHash = getTransactionHash(to, value, data, operation, safeTxGas, dataGas, gasPrice, gasToken, nonce);
49-
checkHash(txHash, v, r, s);
48+
checkHash(txHash, signatures);
5049
// Increase nonce and execute transaction.
5150
nonce++;
5251
require(gasleft() >= safeTxGas, "Not enough gas to execute safe transaction");
53-
if (!execute(to, value, data, operation, safeTxGas)) {
52+
success = execute(to, value, data, operation, safeTxGas);
53+
if (!success) {
5454
emit ExecutionFailed(txHash);
5555
}
5656

@@ -73,7 +73,7 @@ contract GnosisSafePersonalEdition is MasterCopy, GnosisSafe {
7373
/// 1.) The method can only be called from the safe itself
7474
/// 2.) The response is returned with a revert
7575
/// When estimating set `from` to the address of the safe.
76-
/// Since the `estimateGas` function includes refunds, call this method to get an estimated of the costs that are deducted from the safe with `execAndPayTransaction`
76+
/// Since the `estimateGas` function includes refunds, call this method to get an estimated of the costs that are deducted from the safe with `execTransactionAndPaySubmitter`
7777
/// @param to Destination address of Safe transaction.
7878
/// @param value Ether value of Safe transaction.
7979
/// @param data Data payload of Safe transaction.
@@ -92,7 +92,7 @@ contract GnosisSafePersonalEdition is MasterCopy, GnosisSafe {
9292
revert(string(abi.encodePacked(requiredGas)));
9393
}
9494

95-
function checkHash(bytes32 hash, uint8[] v, bytes32[] r, bytes32[] s)
95+
function checkHash(bytes32 txHash, bytes signatures)
9696
internal
9797
view
9898
{
@@ -102,7 +102,7 @@ contract GnosisSafePersonalEdition is MasterCopy, GnosisSafe {
102102
uint256 i;
103103
// Validate threshold is reached.
104104
for (i = 0; i < threshold; i++) {
105-
currentOwner = ecrecover(hash, v[i], r[i], s[i]);
105+
currentOwner = recoverKey(txHash, signatures, i);
106106
require(owners[currentOwner] != 0, "Signature not provided by owner");
107107
require(currentOwner > lastOwner, "Signatures are not ordered by owner address");
108108
lastOwner = currentOwner;

contracts/SignatureValidator.sol

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
pragma solidity 0.4.24;
2+
3+
4+
/// @title SignatureValidator - recovers a sender from a signature
5+
/// @author Ricardo Guilherme Schmidt (Status Research & Development GmbH)
6+
/// @author Richard Meissner - <richard@gnosis.pm>
7+
contract SignatureValidator {
8+
9+
/// @dev Recovers address who signed the message
10+
/// @param txHash operation ethereum signed message hash
11+
/// @param messageSignature message `txHash` signature
12+
/// @param pos which signature to read
13+
function recoverKey (
14+
bytes32 txHash,
15+
bytes messageSignature,
16+
uint256 pos
17+
)
18+
pure
19+
public
20+
returns (address)
21+
{
22+
uint8 v;
23+
bytes32 r;
24+
bytes32 s;
25+
(v, r, s) = signatureSplit(messageSignature, pos);
26+
return ecrecover(txHash, v, r, s);
27+
}
28+
29+
/// @dev divides bytes signature into `uint8 v, bytes32 r, bytes32 s`
30+
/// @param pos which signature to read
31+
/// @param signatures concatenated rsv signatures
32+
function signatureSplit(bytes signatures, uint256 pos)
33+
pure
34+
public
35+
returns (uint8 v, bytes32 r, bytes32 s)
36+
{
37+
// The signature format is a compact form of:
38+
// {bytes32 r}{bytes32 s}{uint8 v}
39+
// Compact means, uint8 is not padded to 32 bytes.
40+
// solium-disable-next-line security/no-inline-assembly
41+
assembly {
42+
let signaturePos := mul(0x41, pos)
43+
r := mload(add(signatures, add(signaturePos, 0x20)))
44+
s := mload(add(signatures, add(signaturePos, 0x40)))
45+
// Here we are loading the last 32 bytes, including 31 bytes
46+
// of 's'. There is no 'mload8' to do this.
47+
//
48+
// 'byte' is not working due to the Solidity parser, so lets
49+
// use the second best option, 'and'
50+
v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff)
51+
}
52+
}
53+
}

contracts/modules/StateChannelModule.sol

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
pragma solidity 0.4.24;
22
import "../Module.sol";
33
import "../OwnerManager.sol";
4+
import "../SignatureValidator.sol";
45

56

67
/// @title Gnosis Safe State Module - A module that allows interaction with statechannels.
78
/// @author Stefan George - <stefan@gnosis.pm>
89
/// @author Richard Meissner - <richard@gnosis.pm>
9-
contract StateChannelModule is Module {
10+
contract StateChannelModule is Module, SignatureValidator {
1011

1112
string public constant NAME = "State Channel Module";
1213
string public constant VERSION = "0.0.1";
@@ -27,30 +28,26 @@ contract StateChannelModule is Module {
2728
/// @param data Data payload of Safe transaction.
2829
/// @param operation Operation type of Safe transaction.
2930
/// @param nonce Nonce used for this Safe transaction.
30-
/// @param v Array of signature V values sorted by owner addresses.
31-
/// @param r Array of signature R values sorted by owner addresses.
32-
/// @param s Array of signature S values sorted by owner addresses.
31+
/// @param signatures Packed signature data ({bytes32 r}{bytes32 s}{uint8 v})
3332
function execTransaction(
3433
address to,
3534
uint256 value,
3635
bytes data,
3736
Enum.Operation operation,
3837
uint256 nonce,
39-
uint8[] v,
40-
bytes32[] r,
41-
bytes32[] s
38+
bytes signatures
4239
)
4340
public
4441
{
4542
bytes32 transactionHash = getTransactionHash(to, value, data, operation, nonce);
4643
require(isExecuted[transactionHash] == 0, "Transaction already executed");
47-
checkHash(transactionHash, v, r, s);
44+
checkHash(transactionHash, signatures);
4845
// Mark as executed and execute transaction.
4946
isExecuted[transactionHash] = 1;
5047
require(manager.execTransactionFromModule(to, value, data, operation), "Could not execute transaction");
5148
}
5249

53-
function checkHash(bytes32 transactionHash, uint8[] v, bytes32[] r, bytes32[] s)
50+
function checkHash(bytes32 transactionHash, bytes signatures)
5451
internal
5552
view
5653
{
@@ -61,7 +58,7 @@ contract StateChannelModule is Module {
6158
uint8 threshold = OwnerManager(manager).getThreshold();
6259
// Validate threshold is reached.
6360
for (i = 0; i < threshold; i++) {
64-
currentOwner = ecrecover(transactionHash, v[i], r[i], s[i]);
61+
currentOwner = recoverKey(transactionHash, signatures, i);
6562
require(OwnerManager(manager).isOwner(currentOwner), "Signature not provided by owner");
6663
require(currentOwner > lastOwner, "Signatures are not ordered by owner address");
6764
lastOwner = currentOwner;

test/dailyLimitModule.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ contract('DailyLimitModule', function(accounts) {
8989
let sigs = utils.signTransaction(lw, [lw.accounts[0], lw.accounts[1]], transactionHash)
9090

9191
utils.logGasUsage(
92-
'execAndPayTransaction change daily limit',
93-
await gnosisSafe.execAndPayTransaction(
94-
dailyLimitModule.address, 0, data, CALL, 100000, 0, web3.toWei(100, 'gwei'), 0, sigs.sigV, sigs.sigR, sigs.sigS
92+
'execTransactionAndPaySubmitter change daily limit',
93+
await gnosisSafe.execTransactionAndPaySubmitter(
94+
dailyLimitModule.address, 0, data, CALL, 100000, 0, web3.toWei(100, 'gwei'), 0, sigs
9595
)
9696
)
9797
dailyLimit = await dailyLimitModule.dailyLimits(0)
@@ -127,7 +127,7 @@ contract('DailyLimitModule', function(accounts) {
127127
let nonce = await gnosisSafe.nonce()
128128
transactionHash = await gnosisSafe.getTransactionHash(dailyLimitModule.address, 0, data, CALL, 100000, 0, 0, 0, nonce)
129129
let sigs = utils.signTransaction(lw, [lw.accounts[0], lw.accounts[1]], transactionHash)
130-
await gnosisSafe.execAndPayTransaction(dailyLimitModule.address, 0, data, CALL, 100000, 0, 0, 0, sigs.sigV, sigs.sigR, sigs.sigS)
130+
await gnosisSafe.execTransactionAndPaySubmitter(dailyLimitModule.address, 0, data, CALL, 100000, 0, 0, 0, sigs)
131131

132132
// Withdrawal should fail as there are no tokens
133133
assert.equal(await testToken.balances(gnosisSafe.address), 0);

test/gnosisSafeSigningTest.js

Lines changed: 0 additions & 40 deletions
This file was deleted.

test/multiSend.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ contract('MultiSend', function(accounts) {
6666
let sigs = utils.signTransaction(lw, [lw.accounts[0]], transactionHash)
6767
utils.logGasUsage(
6868
'execTransaction send multiple transactions',
69-
await gnosisSafe.execAndPayTransaction(
70-
multiSend.address, 0, data, DELEGATECALL, 1000000, 0, 0, 0, sigs.sigV, sigs.sigR, sigs.sigS
69+
await gnosisSafe.execTransactionAndPaySubmitter(
70+
multiSend.address, 0, data, DELEGATECALL, 1000000, 0, 0, 0, sigs
7171
)
7272
)
7373
assert.equal(await web3.eth.getBalance(gnosisSafe.address).toNumber(), 0)
@@ -88,8 +88,8 @@ contract('MultiSend', function(accounts) {
8888
let transactionHash = await gnosisSafe.getTransactionHash(multiSend.address, 0, data, DELEGATECALL, 1000000, 0, 0, 0, nonce)
8989
let sigs = utils.signTransaction(lw, [lw.accounts[0]], transactionHash)
9090
utils.checkTxEvent(
91-
await gnosisSafe.execAndPayTransaction(
92-
multiSend.address, 0, data, DELEGATECALL, 1000000, 0, 0, 0, sigs.sigV, sigs.sigR, sigs.sigS
91+
await gnosisSafe.execTransactionAndPaySubmitter(
92+
multiSend.address, 0, data, DELEGATECALL, 1000000, 0, 0, 0, sigs
9393
),
9494
'ExecutionFailed', gnosisSafe.address, true, 'execTransaction send multiple transactions'
9595
)
@@ -112,8 +112,8 @@ contract('MultiSend', function(accounts) {
112112
let transactionHash = await gnosisSafe.getTransactionHash(multiSend.address, 0, data, DELEGATECALL, 1000000, 0, 0, 0, nonce)
113113
let sigs = utils.signTransaction(lw, [lw.accounts[0]], transactionHash)
114114
utils.checkTxEvent(
115-
await gnosisSafe.execAndPayTransaction(
116-
multiSend.address, 0, data, DELEGATECALL, 1000000, 0, 0, 0, sigs.sigV, sigs.sigR, sigs.sigS
115+
await gnosisSafe.execTransactionAndPaySubmitter(
116+
multiSend.address, 0, data, DELEGATECALL, 1000000, 0, 0, 0, sigs
117117
),
118118
'ExecutionFailed', gnosisSafe.address, true, 'execTransaction send multiple transactions'
119119
)

test/safeMethodNaming.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,12 @@ contract('GnosisSafeEditions', function(accounts) {
3030
it('check method naming of personal safe', async () => {
3131
let functions = getSortedFunctions(GnosisSafePersonal.abi)
3232
console.log(functions)
33-
assert.equal('execAndPayTransaction', functions[0].name)
33+
assert.equal('execTransactionAndPaySubmitter', functions[0].name)
3434
})
3535
it('check method naming of team safe', async () => {
3636
let functions = getSortedFunctions(GnosisSafeTeam.abi)
3737
console.log(functions)
3838
assert.equal('approveTransactionWithParameters', functions[0].name)
3939
assert.equal('execTransactionIfApproved', functions[1].name)
4040
})
41-
it('check method naming of sate channel module', async () => {
42-
let functions = getSortedFunctions(StateChannelModule.abi)
43-
console.log(functions)
44-
assert.equal('execTransaction', functions[0].name)
45-
})
4641
});

test/stateChannelModule.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ contract('StateChannelModule', function(accounts) {
2727
// Execute paying transaction
2828
// We add the minGasEstimate and an additional 10k to the estimate to ensure that there is enough gas for the safe transaction
2929
let tx = stateChannelModule.execTransaction(
30-
to, value, data, operation, nonce, sigs.sigV, sigs.sigR, sigs.sigS, {from: executor}
30+
to, value, data, operation, nonce, sigs, {from: executor}
3131
)
3232

3333
let res

test/utils.js

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,21 +90,13 @@ async function createLightwallet() {
9090
}
9191

9292
function signTransaction(lw, signers, transactionHash) {
93-
let sigV = []
94-
let sigR = []
95-
let sigS = []
93+
let signatureBytes = "0x"
9694
signers.sort()
9795
for (var i=0; i<signers.length; i++) {
9896
let sig = lightwallet.signing.signMsgHash(lw.keystore, lw.passwords, transactionHash, signers[i])
99-
sigV.push(sig.v)
100-
sigR.push('0x' + sig.r.toString('hex'))
101-
sigS.push('0x' + sig.s.toString('hex'))
102-
}
103-
return {
104-
sigV: sigV,
105-
sigR: sigR,
106-
sigS: sigS
97+
signatureBytes += sig.r.toString('hex') + sig.s.toString('hex') + sig.v.toString(16)
10798
}
99+
return signatureBytes
108100
}
109101

110102
async function assertRejects(q, msg) {

0 commit comments

Comments
 (0)