Skip to content

Commit ec029a6

Browse files
feat(contracts): add recovery contracts for v1/v2 wallets versions
1 parent b3f42e5 commit ec029a6

File tree

5 files changed

+330
-0
lines changed

5 files changed

+330
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity 0.8.10;
3+
import './RecoveryWalletSimple.sol';
4+
import '../CloneFactory.sol';
5+
6+
contract RecoveryWalletFactory is CloneFactory {
7+
address public implementationAddress;
8+
9+
constructor(address _implementationAddress) {
10+
implementationAddress = _implementationAddress;
11+
}
12+
13+
function createWallet(address[] calldata allowedSigners, bytes32 salt)
14+
external
15+
{
16+
// include the signers in the salt so any contract deployed to a given address must have the same signers
17+
bytes32 finalSalt = keccak256(abi.encodePacked(allowedSigners, salt));
18+
19+
address payable clone = createClone(implementationAddress, finalSalt);
20+
RecoveryWalletSimple(clone).init(allowedSigners[2]);
21+
}
22+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity 0.8.10;
3+
import '../IForwarder.sol';
4+
5+
/** ERC721, ERC1155 imports */
6+
import '@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol';
7+
import '@openzeppelin/contracts/token/ERC1155/utils/ERC1155Receiver.sol';
8+
9+
/**
10+
*
11+
* RecoveryWallet
12+
* ============
13+
*
14+
* Basic singleSig wallet designed to recover funds.
15+
*
16+
*/
17+
contract RecoveryWalletSimple is IERC721Receiver, ERC1155Receiver {
18+
// Public fields
19+
address public signer;
20+
bool public initialized = false; // True if the contract has been initialized
21+
22+
function init(address _signer) external onlyUninitialized {
23+
signer = _signer;
24+
initialized = true;
25+
}
26+
27+
/**
28+
* Modifier that will execute internal code block only if the sender is an authorized signer on this wallet
29+
*/
30+
modifier onlySigner() {
31+
require(signer == msg.sender, 'Non-signer in onlySigner method');
32+
_;
33+
}
34+
35+
/**
36+
* Modifier that will execute internal code block only if the contract has not been initialized yet
37+
*/
38+
modifier onlyUninitialized() {
39+
require(!initialized, 'Contract already initialized');
40+
_;
41+
}
42+
43+
/**
44+
* Gets called when a transaction is received with data that does not match any other method
45+
*/
46+
fallback() external payable {}
47+
48+
/**
49+
* Gets called when a transaction is received with ether and no data
50+
*/
51+
receive() external payable {}
52+
53+
/**
54+
* Execute a transaction from this wallet using the signer.
55+
*
56+
* @param toAddress the destination address to send an outgoing transaction
57+
* @param value the amount in Wei to be sent
58+
* @param data the data to send to the toAddress when invoking the transaction
59+
*/
60+
function sendFunds(
61+
address toAddress,
62+
uint256 value,
63+
bytes calldata data
64+
) external onlySigner {
65+
// Success, send the transaction
66+
(bool success, ) = toAddress.call{ value: value }(data);
67+
require(success, 'Call execution failed');
68+
}
69+
70+
/**
71+
* Execute a token flush from one of the forwarder addresses. This transfer can be done by any external address
72+
*
73+
* @param forwarderAddress the address of the forwarder address to flush the tokens from
74+
* @param tokenContractAddress the address of the erc20 token contract
75+
*/
76+
function flushForwarderTokens(
77+
address payable forwarderAddress,
78+
address tokenContractAddress
79+
) external {
80+
IForwarder forwarder = IForwarder(forwarderAddress);
81+
forwarder.flushTokens(tokenContractAddress);
82+
}
83+
84+
/**
85+
* Execute a ERC721 token flush from one of the forwarder addresses. This transfer can be done by any external address
86+
*
87+
* @param forwarderAddress the address of the forwarder address to flush the tokens from
88+
* @param tokenContractAddress the address of the erc20 token contract
89+
*/
90+
function flushERC721ForwarderTokens(
91+
address payable forwarderAddress,
92+
address tokenContractAddress,
93+
uint256 tokenId
94+
) external {
95+
IForwarder forwarder = IForwarder(forwarderAddress);
96+
forwarder.flushERC721Token(tokenContractAddress, tokenId);
97+
}
98+
99+
/**
100+
* Execute a ERC1155 batch token flush from one of the forwarder addresses.
101+
* This transfer can be done by any external address.
102+
*
103+
* @param forwarderAddress the address of the forwarder address to flush the tokens from
104+
* @param tokenContractAddress the address of the erc1155 token contract
105+
*/
106+
function batchFlushERC1155ForwarderTokens(
107+
address payable forwarderAddress,
108+
address tokenContractAddress,
109+
uint256[] calldata tokenIds
110+
) external {
111+
IForwarder forwarder = IForwarder(forwarderAddress);
112+
forwarder.batchFlushERC1155Tokens(tokenContractAddress, tokenIds);
113+
}
114+
115+
/**
116+
* Execute a ERC1155 token flush from one of the forwarder addresses.
117+
* This transfer can be done by any external address.
118+
*
119+
* @param forwarderAddress the address of the forwarder address to flush the tokens from
120+
* @param tokenContractAddress the address of the erc1155 token contract
121+
* @param tokenId the token id associated with the ERC1155
122+
*/
123+
function flushERC1155ForwarderTokens(
124+
address payable forwarderAddress,
125+
address tokenContractAddress,
126+
uint256 tokenId
127+
) external {
128+
IForwarder forwarder = IForwarder(forwarderAddress);
129+
forwarder.flushERC1155Tokens(tokenContractAddress, tokenId);
130+
}
131+
132+
/**
133+
* Sets the autoflush 721 parameter on the forwarder.
134+
*
135+
* @param forwarderAddress the address of the forwarder to toggle.
136+
* @param autoFlush whether to autoflush erc721 tokens
137+
*/
138+
function setAutoFlush721(address forwarderAddress, bool autoFlush)
139+
external
140+
onlySigner
141+
{
142+
IForwarder forwarder = IForwarder(forwarderAddress);
143+
forwarder.setAutoFlush721(autoFlush);
144+
}
145+
146+
/**
147+
* Sets the autoflush 721 parameter on the forwarder.
148+
*
149+
* @param forwarderAddress the address of the forwarder to toggle.
150+
* @param autoFlush whether to autoflush erc1155 tokens
151+
*/
152+
function setAutoFlush1155(address forwarderAddress, bool autoFlush)
153+
external
154+
onlySigner
155+
{
156+
IForwarder forwarder = IForwarder(forwarderAddress);
157+
forwarder.setAutoFlush1155(autoFlush);
158+
}
159+
160+
/**
161+
* ERC721 standard callback function for when a ERC721 is transfered.
162+
*
163+
* @param _operator The address of the nft contract
164+
* @param _from The address of the sender
165+
* @param _tokenId The token id of the nft
166+
* @param _data Additional data with no specified format, sent in call to `_to`
167+
*/
168+
function onERC721Received(
169+
address _operator,
170+
address _from,
171+
uint256 _tokenId,
172+
bytes memory _data
173+
) external virtual override returns (bytes4) {
174+
return this.onERC721Received.selector;
175+
}
176+
177+
/**
178+
* @inheritdoc IERC1155Receiver
179+
*/
180+
function onERC1155Received(
181+
address _operator,
182+
address _from,
183+
uint256 id,
184+
uint256 value,
185+
bytes calldata data
186+
) external virtual override returns (bytes4) {
187+
return this.onERC1155Received.selector;
188+
}
189+
190+
/**
191+
* @inheritdoc IERC1155Receiver
192+
*/
193+
function onERC1155BatchReceived(
194+
address _operator,
195+
address _from,
196+
uint256[] calldata ids,
197+
uint256[] calldata values,
198+
bytes calldata data
199+
) external virtual override returns (bytes4) {
200+
return this.onERC1155BatchReceived.selector;
201+
}
202+
}

test/wallet/helpers.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const crypto = require('crypto');
1111
const Forwarder = artifacts.require('../Forwarder.sol');
1212
const ForwarderFactory = artifacts.require('../ForwarderFactory.sol');
1313
const WalletFactory = artifacts.require('../WalletFactory.sol');
14+
const RecoveryWalletFactory = artifacts.require('../RecoveryWalletFactory.sol');
15+
const RecoveryWalletSimple = artifacts.require('../RecoveryWalletSimple.sol');
1416

1517
const assertVMException = (err, expectedErrMsg) => {
1618
err.message.toString().should.containEql('VM Exception');
@@ -84,6 +86,41 @@ const createWalletFactory = async (WalletSimple) => {
8486
};
8587
};
8688

89+
const createRecoveryWalletFactory = async () => {
90+
const walletContract = await RecoveryWalletSimple.new([], {});
91+
const walletFactory = await RecoveryWalletFactory.new(walletContract.address);
92+
return {
93+
implementationAddress: walletContract.address,
94+
factory: walletFactory
95+
};
96+
};
97+
98+
const createRecoveryWalletHelper = async (creator, signers) => {
99+
// OK to be the same for all wallets since we are using a new factory for each
100+
const salt = '0x1234';
101+
const { factory, implementationAddress } =
102+
await createRecoveryWalletFactory();
103+
104+
const inputSalt = util.setLengthLeft(
105+
Buffer.from(util.stripHexPrefix(salt), 'hex'),
106+
32
107+
);
108+
const calculationSalt = abi.soliditySHA3(
109+
['address[]', 'bytes32'],
110+
[signers, inputSalt]
111+
);
112+
const initCode = helpers.getInitCode(
113+
util.stripHexPrefix(implementationAddress)
114+
);
115+
const walletAddress = helpers.getNextContractAddressCreate2(
116+
factory.address,
117+
calculationSalt,
118+
initCode
119+
);
120+
await factory.createWallet(signers, inputSalt, { from: creator });
121+
return RecoveryWalletSimple.at(walletAddress);
122+
};
123+
87124
const createWalletHelper = async (WalletSimple, creator, signers) => {
88125
// OK to be the same for all wallets since we are using a new factory for each
89126
const salt = '0x1234';
@@ -130,7 +167,9 @@ exports.assertVMException = assertVMException;
130167
exports.createForwarderFromWallet = createForwarderFromWallet;
131168
exports.executeCreateForwarder = executeCreateForwarder;
132169
exports.createWalletFactory = createWalletFactory;
170+
exports.createRecoveryWalletFactory = createRecoveryWalletFactory;
133171
exports.createWalletHelper = createWalletHelper;
172+
exports.createRecoveryWalletHelper = createRecoveryWalletHelper;
134173
exports.getBalanceInWei = getBalanceInWei;
135174
exports.calculateFutureExpireTime = calculateFutureExpireTime;
136175
exports.isSigner = isSigner;

test/walletFactory.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const helpers = require('./helpers');
44
const util = require('ethereumjs-util');
55
const abi = require('ethereumjs-abi');
66
const { privateKeyForAccount } = require('./helpers');
7+
const { createRecoveryWalletHelper } = require('./wallet/helpers');
78
const BigNumber = require('bignumber.js');
89
const hre = require('hardhat');
910

@@ -220,3 +221,17 @@ describe('WalletFactory', function () {
220221
);
221222
});
222223
});
224+
225+
describe('RecoveryWalletFactory', function () {
226+
let accounts;
227+
before(async () => {
228+
await hre.network.provider.send('hardhat_reset');
229+
accounts = await web3.eth.getAccounts();
230+
});
231+
it('Should create a wallet using factory', async function () {
232+
const signers = [accounts[0], accounts[1], accounts[2]];
233+
const wallet = await createRecoveryWalletHelper(accounts[0], signers);
234+
const signer = await wallet.signer.call();
235+
signer.should.equal(accounts[2]);
236+
});
237+
});

test/walletSimple.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
calculateFutureExpireTime,
1414
createForwarderFromWallet,
1515
createWalletHelper,
16+
createRecoveryWalletHelper,
1617
getBalanceInWei,
1718
isSigner
1819
} = require('./wallet/helpers');
@@ -2742,3 +2743,54 @@ coins.forEach(
27422743
});
27432744
}
27442745
);
2746+
2747+
describe('RecoveryWallet', function () {
2748+
before(async () => {
2749+
await hre.network.provider.send('hardhat_reset');
2750+
accounts = await web3.eth.getAccounts();
2751+
});
2752+
it('Should successfully send funds', async function () {
2753+
const signers = [accounts[0], accounts[1], accounts[2]];
2754+
const wallet = await createRecoveryWalletHelper(accounts[0], signers);
2755+
const signer = await wallet.signer.call();
2756+
signer.should.equal(accounts[2]);
2757+
const tx = await web3.eth.sendTransaction({
2758+
from: accounts[0],
2759+
to: wallet.address,
2760+
value: web3.utils.toWei('20', 'ether')
2761+
});
2762+
const walletStartBalance = await web3.eth.getBalance(wallet.address);
2763+
const amount = web3.utils.toWei('2', 'ether');
2764+
await wallet.sendFunds(accounts[1], amount, '0x', {
2765+
from: accounts[2]
2766+
});
2767+
// Check wallet balance
2768+
const walletEndBalance = await web3.eth.getBalance(wallet.address);
2769+
new BigNumber(walletStartBalance)
2770+
.minus(amount)
2771+
.eq(walletEndBalance)
2772+
.should.be.true();
2773+
});
2774+
2775+
it('Should fail to send funds using wrong signer', async function () {
2776+
const signers = [accounts[0], accounts[1], accounts[2]];
2777+
const wallet = await createRecoveryWalletHelper(accounts[0], signers);
2778+
const signer = await wallet.signer.call();
2779+
signer.should.equal(accounts[2]);
2780+
const tx = await web3.eth.sendTransaction({
2781+
from: accounts[0],
2782+
to: wallet.address,
2783+
value: web3.utils.toWei('20', 'ether')
2784+
});
2785+
try {
2786+
await wallet.sendFunds(
2787+
accounts[1],
2788+
web3.utils.toWei('2', 'ether'),
2789+
'0x',
2790+
{ from: accounts[1] }
2791+
);
2792+
} catch (err) {
2793+
assertVMException(err, 'Non-signer in onlySigner method');
2794+
}
2795+
});
2796+
});

0 commit comments

Comments
 (0)