Skip to content

Commit 8544002

Browse files
Add Support for CELO, RSK and ETC
2 parents df532e9 + 479e513 commit 8544002

7 files changed

Lines changed: 1719 additions & 727 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ A test suite is included through the use of the truffle framework, providing cov
1919

2020
## Installation
2121

22-
NodeJS 8.9.0 is recommended.
22+
NodeJS 8.14.0 is recommended.
2323

2424
```shell
2525
npm install
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
pragma solidity ^0.4.18;
2+
import "../Forwarder.sol";
3+
import "../ERC20Interface.sol";
4+
/**
5+
*
6+
* WalletSimple
7+
* ============
8+
*
9+
* Basic multi-signer wallet designed for use in a co-signing environment where 2 signatures are required to move funds.
10+
* Typically used in a 2-of-3 signing configuration. Uses ecrecover to allow for 2 signatures in a single transaction.
11+
*
12+
* The first signature is created on the operation hash (see Data Formats) and passed to sendMultiSig/sendMultiSigToken
13+
* The signer is determined by verifyMultiSig().
14+
*
15+
* The second signature is created by the submitter of the transaction and determined by msg.signer.
16+
*
17+
* Data Formats
18+
* ============
19+
*
20+
* The signature is created with ethereumjs-util.ecsign(operationHash).
21+
* Like the eth_sign RPC call, it packs the values as a 65-byte array of [r, s, v].
22+
* Unlike eth_sign, the message is not prefixed.
23+
*
24+
* The operationHash the result of keccak256(prefix, toAddress, value, data, expireTime).
25+
* For ether transactions, `prefix` is "ETHER".
26+
* For token transaction, `prefix` is "ERC20" and `data` is the tokenContractAddress.
27+
*
28+
*
29+
*/
30+
contract CeloWalletSimple {
31+
// Events
32+
event Deposited(address from, uint value, bytes data);
33+
event SafeModeActivated(address msgSender);
34+
event Transacted(
35+
address msgSender, // Address of the sender of the message initiating the transaction
36+
address otherSigner, // Address of the signer (second signature) used to initiate the transaction
37+
bytes32 operation, // Operation hash (see Data Formats)
38+
address toAddress, // The address the transaction was sent to
39+
uint value, // Amount of Wei sent to the address
40+
bytes data // Data sent when invoking the transaction
41+
);
42+
43+
// Public fields
44+
address[] public signers; // The addresses that can co-sign transactions on the wallet
45+
bool public safeMode = false; // When active, wallet may only send to signer addresses
46+
47+
// Internal fields
48+
uint constant SEQUENCE_ID_WINDOW_SIZE = 10;
49+
uint[10] recentSequenceIds;
50+
51+
/**
52+
* Set up a simple multi-sig wallet by specifying the signers allowed to be used on this wallet.
53+
* 2 signers will be required to send a transaction from this wallet.
54+
* Note: The sender is NOT automatically added to the list of signers.
55+
* Signers CANNOT be changed once they are set
56+
*
57+
* @param allowedSigners An array of signers on the wallet
58+
*/
59+
function CeloWalletSimple(address[] allowedSigners) public {
60+
if (allowedSigners.length != 3) {
61+
// Invalid number of signers
62+
revert();
63+
}
64+
signers = allowedSigners;
65+
}
66+
67+
/**
68+
* Determine if an address is a signer on this wallet
69+
* @param signer address to check
70+
* returns boolean indicating whether address is signer or not
71+
*/
72+
function isSigner(address signer) public view returns (bool) {
73+
// Iterate through all signers on the wallet and
74+
for (uint i = 0; i < signers.length; i++) {
75+
if (signers[i] == signer) {
76+
return true;
77+
}
78+
}
79+
return false;
80+
}
81+
82+
/**
83+
* Modifier that will execute internal code block only if the sender is an authorized signer on this wallet
84+
*/
85+
modifier onlySigner {
86+
if (!isSigner(msg.sender)) {
87+
revert();
88+
}
89+
_;
90+
}
91+
92+
/**
93+
* Gets called when a transaction is received without calling a method
94+
*/
95+
function() public payable {
96+
if (msg.value > 0) {
97+
// Fire deposited event if we are receiving funds
98+
Deposited(msg.sender, msg.value, msg.data);
99+
}
100+
}
101+
102+
/**
103+
* Create a new contract (and also address) that forwards funds to this contract
104+
* returns address of newly created forwarder address
105+
*/
106+
function createForwarder() public returns (address) {
107+
return new Forwarder();
108+
}
109+
110+
/**
111+
* Execute a multi-signature transaction from this wallet using 2 signers: one from msg.sender and the other from ecrecover.
112+
* Sequence IDs are numbers starting from 1. They are used to prevent replay attacks and may not be repeated.
113+
*
114+
* @param toAddress the destination address to send an outgoing transaction
115+
* @param value the amount in Wei to be sent
116+
* @param data the data to send to the toAddress when invoking the transaction
117+
* @param expireTime the number of seconds since 1970 for which this transaction is valid
118+
* @param sequenceId the unique sequence id obtainable from getNextSequenceId
119+
* @param signature see Data Formats
120+
*/
121+
function sendMultiSig(
122+
address toAddress,
123+
uint value,
124+
bytes data,
125+
uint expireTime,
126+
uint sequenceId,
127+
bytes signature
128+
) public onlySigner {
129+
// Verify the other signer
130+
var operationHash = keccak256("CELO", toAddress, value, data, expireTime, sequenceId);
131+
132+
var otherSigner = verifyMultiSig(toAddress, operationHash, signature, expireTime, sequenceId);
133+
134+
// Success, send the transaction
135+
if (!(toAddress.call.value(value)(data))) {
136+
// Failed executing transaction
137+
revert();
138+
}
139+
Transacted(msg.sender, otherSigner, operationHash, toAddress, value, data);
140+
}
141+
142+
/**
143+
* Execute a multi-signature token transfer from this wallet using 2 signers: one from msg.sender and the other from ecrecover.
144+
* Sequence IDs are numbers starting from 1. They are used to prevent replay attacks and may not be repeated.
145+
*
146+
* @param toAddress the destination address to send an outgoing transaction
147+
* @param value the amount in tokens to be sent
148+
* @param tokenContractAddress the address of the erc20 token contract
149+
* @param expireTime the number of seconds since 1970 for which this transaction is valid
150+
* @param sequenceId the unique sequence id obtainable from getNextSequenceId
151+
* @param signature see Data Formats
152+
*/
153+
function sendMultiSigToken(
154+
address toAddress,
155+
uint value,
156+
address tokenContractAddress,
157+
uint expireTime,
158+
uint sequenceId,
159+
bytes signature
160+
) public onlySigner {
161+
// Verify the other signer
162+
var operationHash = keccak256("CELO-ERC20", toAddress, value, tokenContractAddress, expireTime, sequenceId);
163+
164+
verifyMultiSig(toAddress, operationHash, signature, expireTime, sequenceId);
165+
166+
ERC20Interface instance = ERC20Interface(tokenContractAddress);
167+
if (!instance.transfer(toAddress, value)) {
168+
revert();
169+
}
170+
}
171+
172+
/**
173+
* Execute a token flush from one of the forwarder addresses. This transfer needs only a single signature and can be done by any signer
174+
*
175+
* @param forwarderAddress the address of the forwarder address to flush the tokens from
176+
* @param tokenContractAddress the address of the erc20 token contract
177+
*/
178+
function flushForwarderTokens(
179+
address forwarderAddress,
180+
address tokenContractAddress
181+
) public onlySigner {
182+
Forwarder forwarder = Forwarder(forwarderAddress);
183+
forwarder.flushTokens(tokenContractAddress);
184+
}
185+
186+
/**
187+
* Do common multisig verification for both eth sends and erc20token transfers
188+
*
189+
* @param toAddress the destination address to send an outgoing transaction
190+
* @param operationHash see Data Formats
191+
* @param signature see Data Formats
192+
* @param expireTime the number of seconds since 1970 for which this transaction is valid
193+
* @param sequenceId the unique sequence id obtainable from getNextSequenceId
194+
* returns address that has created the signature
195+
*/
196+
function verifyMultiSig(
197+
address toAddress,
198+
bytes32 operationHash,
199+
bytes signature,
200+
uint expireTime,
201+
uint sequenceId
202+
) private returns (address) {
203+
204+
var otherSigner = recoverAddressFromSignature(operationHash, signature);
205+
206+
// Verify if we are in safe mode. In safe mode, the wallet can only send to signers
207+
if (safeMode && !isSigner(toAddress)) {
208+
// We are in safe mode and the toAddress is not a signer. Disallow!
209+
revert();
210+
}
211+
// Verify that the transaction has not expired
212+
if (expireTime < block.timestamp) {
213+
// Transaction expired
214+
revert();
215+
}
216+
217+
// Try to insert the sequence ID. Will revert if the sequence id was invalid
218+
tryInsertSequenceId(sequenceId);
219+
220+
if (!isSigner(otherSigner)) {
221+
// Other signer not on this wallet or operation does not match arguments
222+
revert();
223+
}
224+
if (otherSigner == msg.sender) {
225+
// Cannot approve own transaction
226+
revert();
227+
}
228+
229+
return otherSigner;
230+
}
231+
232+
/**
233+
* Irrevocably puts contract into safe mode. When in this mode, transactions may only be sent to signing addresses.
234+
*/
235+
function activateSafeMode() public onlySigner {
236+
safeMode = true;
237+
SafeModeActivated(msg.sender);
238+
}
239+
240+
/**
241+
* Gets signer's address using ecrecover
242+
* @param operationHash see Data Formats
243+
* @param signature see Data Formats
244+
* returns address recovered from the signature
245+
*/
246+
function recoverAddressFromSignature(
247+
bytes32 operationHash,
248+
bytes signature
249+
) private pure returns (address) {
250+
if (signature.length != 65) {
251+
revert();
252+
}
253+
// We need to unpack the signature, which is given as an array of 65 bytes (like eth.sign)
254+
bytes32 r;
255+
bytes32 s;
256+
uint8 v;
257+
assembly {
258+
r := mload(add(signature, 32))
259+
s := mload(add(signature, 64))
260+
v := and(mload(add(signature, 65)), 255)
261+
}
262+
if (v < 27) {
263+
v += 27; // Ethereum versions are 27 or 28 as opposed to 0 or 1 which is submitted by some signing libs
264+
}
265+
return ecrecover(operationHash, v, r, s);
266+
}
267+
268+
/**
269+
* Verify that the sequence id has not been used before and inserts it. Throws if the sequence ID was not accepted.
270+
* We collect a window of up to 10 recent sequence ids, and allow any sequence id that is not in the window and
271+
* greater than the minimum element in the window.
272+
* @param sequenceId to insert into array of stored ids
273+
*/
274+
function tryInsertSequenceId(uint sequenceId) private onlySigner {
275+
// Keep a pointer to the lowest value element in the window
276+
uint lowestValueIndex = 0;
277+
for (uint i = 0; i < SEQUENCE_ID_WINDOW_SIZE; i++) {
278+
if (recentSequenceIds[i] == sequenceId) {
279+
// This sequence ID has been used before. Disallow!
280+
revert();
281+
}
282+
if (recentSequenceIds[i] < recentSequenceIds[lowestValueIndex]) {
283+
lowestValueIndex = i;
284+
}
285+
}
286+
if (sequenceId < recentSequenceIds[lowestValueIndex]) {
287+
// The sequence ID being used is lower than the lowest value in the window
288+
// so we cannot accept it as it may have been used before
289+
revert();
290+
}
291+
if (sequenceId > (recentSequenceIds[lowestValueIndex] + 10000)) {
292+
// Block sequence IDs which are much higher than the lowest value
293+
// This prevents people blocking the contract by using very large sequence IDs quickly
294+
revert();
295+
}
296+
recentSequenceIds[lowestValueIndex] = sequenceId;
297+
}
298+
299+
/**
300+
* Gets the next available sequence ID for signing when using executeAndConfirm
301+
* returns the sequenceId one higher than the highest currently stored
302+
*/
303+
function getNextSequenceId() public view returns (uint) {
304+
uint highestSequenceId = 0;
305+
for (uint i = 0; i < SEQUENCE_ID_WINDOW_SIZE; i++) {
306+
if (recentSequenceIds[i] > highestSequenceId) {
307+
highestSequenceId = recentSequenceIds[i];
308+
}
309+
}
310+
return highestSequenceId + 1;
311+
}
312+
}

0 commit comments

Comments
 (0)