Skip to content

Commit 2113b71

Browse files
authored
Merge pull request #1 from mento-protocol/fix/findings
2 parents 94b19e1 + c5fff1f commit 2113b71

File tree

3 files changed

+53
-2
lines changed

3 files changed

+53
-2
lines changed

.env.example

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ CELO_RPC_URL=https://forno.celo.org
55
# Deployer address (Celo: Deployer 1)
66
DEPLOYER=0xE23a4c6615669526Ab58E9c37088bee4eD2b2dEE
77

8-
# Owner of deployed recovery contracts (must differ from DEPLOYER to avoid burning nonces)
9-
RECOVERER=0x0000000000000000000000000000000000000000
8+
# Owner of deployed recovery contracts. Replace this placeholder with your
9+
# actual recoverer address; it must differ from DEPLOYER to avoid burning nonces.
10+
RECOVERER=0x0000000000000000000000000000000000000169
1011

1112
# Deploy recovery contracts up to (and including) this nonce
1213
MAX_NONCE=68

src/RecoveryContract.sol

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,24 @@ contract RecoveryContract is Ownable {
1515

1616
error TransferFailed();
1717
error ExecutionFailed(bytes returnData);
18+
error ZeroAddressRecipient();
19+
error ZeroAddressTarget();
20+
error NoCodeAtTarget(address target);
21+
error EmptyExecution();
1822

1923
/// @param _recoverer The address that owns this contract and can recover funds.
2024
constructor(address _recoverer) Ownable(_recoverer) {}
2125

2226
/// @notice Recover native ETH held by this contract.
2327
function recoverETH(address payable to, uint256 amount) external onlyOwner {
28+
if (to == address(0)) revert ZeroAddressRecipient();
2429
(bool success,) = to.call{value: amount}("");
2530
if (!success) revert TransferFailed();
2631
}
2732

2833
/// @notice Recover any ERC20 token held by this contract.
2934
function recoverERC20(address token, address to, uint256 amount) external onlyOwner {
35+
if (to == address(0)) revert ZeroAddressRecipient();
3036
IERC20(token).safeTransfer(to, amount);
3137
}
3238

@@ -35,6 +41,12 @@ contract RecoveryContract is Ownable {
3541
/// @param value ETH value to send with the call.
3642
/// @param data Calldata to execute (e.g. ERC20.transfer, ERC721.transferFrom).
3743
function execute(address target, uint256 value, bytes calldata data) external onlyOwner returns (bytes memory) {
44+
if (target == address(0)) revert ZeroAddressTarget();
45+
if (target.code.length == 0) {
46+
if (data.length > 0) revert NoCodeAtTarget(target);
47+
if (value == 0) revert EmptyExecution();
48+
}
49+
3850
(bool success, bytes memory result) = target.call{value: value}(data);
3951
if (!success) revert ExecutionFailed(result);
4052
return result;

test/RecoveryContract.t.sol

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ contract RecoveryContractTest is Test {
4848
assertEq(address(rc).balance, 1.5 ether);
4949
}
5050

51+
function test_recoverETH_zeroRecipientReverts() public {
52+
vm.deal(address(rc), 1 ether);
53+
54+
vm.prank(OWNER);
55+
vm.expectRevert(abi.encodeWithSelector(RecoveryContract.ZeroAddressRecipient.selector));
56+
rc.recoverETH(payable(address(0)), 1 ether);
57+
}
58+
5159
function test_recoverETH_unauthorized() public {
5260
vm.deal(address(rc), 1 ether);
5361

@@ -68,6 +76,14 @@ contract RecoveryContractTest is Test {
6876
assertEq(token.balanceOf(address(rc)), 0);
6977
}
7078

79+
function test_recoverERC20_zeroRecipientReverts() public {
80+
token.mint(address(rc), 1000e18);
81+
82+
vm.prank(OWNER);
83+
vm.expectRevert(abi.encodeWithSelector(RecoveryContract.ZeroAddressRecipient.selector));
84+
rc.recoverERC20(address(token), address(0), 1000e18);
85+
}
86+
7187
function test_recoverERC20_unauthorized() public {
7288
token.mint(address(rc), 1000e18);
7389

@@ -96,6 +112,28 @@ contract RecoveryContractTest is Test {
96112
assertEq(nft.ownerOf(42), RECIPIENT);
97113
}
98114

115+
function test_execute_zeroTargetReverts() public {
116+
vm.prank(OWNER);
117+
vm.expectRevert(abi.encodeWithSelector(RecoveryContract.ZeroAddressTarget.selector));
118+
rc.execute(address(0), 0, "");
119+
}
120+
121+
function test_execute_noCodeWithCalldataReverts() public {
122+
address noCodeTarget = makeAddr("noCodeTarget");
123+
124+
vm.prank(OWNER);
125+
vm.expectRevert(abi.encodeWithSelector(RecoveryContract.NoCodeAtTarget.selector, noCodeTarget));
126+
rc.execute(noCodeTarget, 0, hex"1234");
127+
}
128+
129+
function test_execute_noCodeNoValueReverts() public {
130+
address noCodeTarget = makeAddr("noCodeTarget");
131+
132+
vm.prank(OWNER);
133+
vm.expectRevert(abi.encodeWithSelector(RecoveryContract.EmptyExecution.selector));
134+
rc.execute(noCodeTarget, 0, "");
135+
}
136+
99137
function test_execute_unauthorized() public {
100138
vm.deal(address(rc), 1 ether);
101139

0 commit comments

Comments
 (0)