-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: add permissionless rescuable * feat: add permissionless rescuable * fix: assert actual error * fix: add maxRescue * fix: update inheritance
- Loading branch information
Showing
10 changed files
with
276 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.8; | ||
|
||
import {IERC20} from '../oz-common/interfaces/IERC20.sol'; | ||
import {RescuableBase} from './RescuableBase.sol'; | ||
import {IPermissionlessRescuable} from './interfaces/IPermissionlessRescuable.sol'; | ||
|
||
abstract contract PermissionlessRescuable is RescuableBase, IPermissionlessRescuable { | ||
/// @inheritdoc IPermissionlessRescuable | ||
function whoShouldReceiveFunds() public view virtual returns (address); | ||
|
||
/// @inheritdoc IPermissionlessRescuable | ||
function emergencyTokenTransfer(address erc20Token, uint256 amount) external virtual { | ||
_emergencyTokenTransfer(erc20Token, whoShouldReceiveFunds(), amount); | ||
} | ||
|
||
/// @inheritdoc IPermissionlessRescuable | ||
function emergencyEtherTransfer(uint256 amount) external virtual { | ||
_emergencyEtherTransfer(whoShouldReceiveFunds(), amount); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.8; | ||
|
||
import {IERC20} from '../oz-common/interfaces/IERC20.sol'; | ||
import {SafeERC20} from '../oz-common/SafeERC20.sol'; | ||
import {IRescuableBase} from './interfaces/IRescuableBase.sol'; | ||
|
||
abstract contract RescuableBase is IRescuableBase { | ||
using SafeERC20 for IERC20; | ||
|
||
/// @inheritdoc IRescuableBase | ||
function maxRescue(address erc20Token) public view virtual returns (uint256); | ||
|
||
function _emergencyTokenTransfer( | ||
address erc20Token, | ||
address to, | ||
uint256 amount | ||
) internal virtual { | ||
uint256 max = maxRescue(erc20Token); | ||
amount = max > amount ? amount : max; | ||
IERC20(erc20Token).safeTransfer(to, amount); | ||
|
||
emit ERC20Rescued(msg.sender, erc20Token, to, amount); | ||
} | ||
|
||
function _emergencyEtherTransfer(address to, uint256 amount) internal virtual { | ||
(bool success, ) = to.call{value: amount}(new bytes(0)); | ||
require(success, 'ETH_TRANSFER_FAIL'); | ||
|
||
emit NativeTokensRescued(msg.sender, to, amount); | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
src/contracts/utils/interfaces/IPermissionlessRescuable.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.8; | ||
|
||
import {IRescuableBase} from './IRescuableBase.sol'; | ||
|
||
/** | ||
* @title IRescuable | ||
* @author BGD Labs | ||
* @notice interface containing the objects, events and methods definitions of the Rescuable contract | ||
*/ | ||
interface IPermissionlessRescuable is IRescuableBase { | ||
/** | ||
* @notice method called to rescue tokens sent erroneously to the contract. Only callable by owner | ||
* @param erc20Token address of the token to rescue | ||
* @param amount of tokens to rescue | ||
*/ | ||
function emergencyTokenTransfer(address erc20Token, uint256 amount) external; | ||
|
||
/** | ||
* @notice method called to rescue ether sent erroneously to the contract. Only callable by owner | ||
* @param amount of eth to rescue | ||
*/ | ||
function emergencyEtherTransfer(uint256 amount) external; | ||
|
||
/** | ||
* @notice method that defines the address that should receive the rescued tokens | ||
* @return the receiver address | ||
*/ | ||
function whoShouldReceiveFunds() external view returns (address); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.8; | ||
|
||
/** | ||
* @title IRescuableBase | ||
* @author BGD Labs | ||
* @notice interface containing the objects, events and methods definitions of the RescuableBase contract | ||
*/ | ||
interface IRescuableBase { | ||
/** | ||
* @notice emitted when erc20 tokens get rescued | ||
* @param caller address that triggers the rescue | ||
* @param token address of the rescued token | ||
* @param to address that will receive the rescued tokens | ||
* @param amount quantity of tokens rescued | ||
*/ | ||
event ERC20Rescued( | ||
address indexed caller, | ||
address indexed token, | ||
address indexed to, | ||
uint256 amount | ||
); | ||
|
||
/** | ||
* @notice emitted when native tokens get rescued | ||
* @param caller address that triggers the rescue | ||
* @param to address that will receive the rescued tokens | ||
* @param amount quantity of tokens rescued | ||
*/ | ||
event NativeTokensRescued(address indexed caller, address indexed to, uint256 amount); | ||
|
||
/** | ||
* @notice method that defined the maximum amount rescuable for any given asset. | ||
* @dev there's currently no way to limit the rescuable "native asset", as we assume erc20s as intended underlying. | ||
* @return the maximum amount of | ||
*/ | ||
function maxRescue(address erc20Token) external view returns (uint256); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
import 'forge-std/Test.sol'; | ||
import {IERC20} from '../src/contracts/oz-common/interfaces/IERC20.sol'; | ||
import {Address} from '../src/contracts/oz-common/Address.sol'; | ||
import {ERC20} from '../src/mocks/ERC20.sol'; | ||
import {PermissionlessRescuable, IPermissionlessRescuable} from '../src/contracts/utils/PermissionlessRescuable.sol'; | ||
import {RescuableBase, IRescuableBase} from '../src/contracts/utils/RescuableBase.sol'; | ||
|
||
// Concrete implementation of PermissionlessRescuable for testing | ||
contract TestPermissionlessRescuable is PermissionlessRescuable { | ||
address public fundsReceiver; | ||
address public restrictedErc20; | ||
|
||
constructor(address _fundsReceiver, address _restrictedErc20) { | ||
fundsReceiver = _fundsReceiver; | ||
restrictedErc20 = _restrictedErc20; | ||
} | ||
|
||
function whoShouldReceiveFunds() public view override returns (address) { | ||
return fundsReceiver; | ||
} | ||
|
||
/** | ||
* Mock implementation forcing 10 wei leftover | ||
*/ | ||
function maxRescue( | ||
address erc20 | ||
) public view override(RescuableBase, IRescuableBase) returns (uint256) { | ||
if (erc20 == restrictedErc20) { | ||
uint256 balance = ERC20(erc20).balanceOf(address(this)); | ||
return balance > 10 ? balance - 10 : 0; | ||
} | ||
return type(uint256).max; | ||
} | ||
|
||
// Function to receive Ether | ||
receive() external payable {} | ||
} | ||
|
||
contract PermissionlessRescuableTest is Test { | ||
TestPermissionlessRescuable public rescuable; | ||
ERC20 public mockToken; | ||
ERC20 public restrictedMockToken; | ||
address public fundsReceiver; | ||
|
||
event ERC20Rescued( | ||
address indexed caller, | ||
address indexed token, | ||
address indexed to, | ||
uint256 amount | ||
); | ||
event NativeTokensRescued(address indexed caller, address indexed to, uint256 amount); | ||
|
||
function setUp() public { | ||
fundsReceiver = address(0x123); | ||
mockToken = new ERC20('Test', 'TST'); | ||
restrictedMockToken = new ERC20('Test', 'TST'); | ||
rescuable = new TestPermissionlessRescuable(fundsReceiver, address(restrictedMockToken)); | ||
} | ||
|
||
function test_whoShouldReceiveFunds() public { | ||
assertEq(rescuable.whoShouldReceiveFunds(), fundsReceiver); | ||
} | ||
|
||
function test_emergencyTokenTransfer() public { | ||
uint256 amount = 100; | ||
mockToken.mint(address(rescuable), amount); | ||
|
||
vm.expectEmit(true, true, true, true); | ||
emit ERC20Rescued(address(this), address(mockToken), fundsReceiver, amount); | ||
|
||
rescuable.emergencyTokenTransfer(address(mockToken), amount); | ||
|
||
assertEq(mockToken.balanceOf(fundsReceiver), amount); | ||
assertEq(mockToken.balanceOf(address(rescuable)), 0); | ||
} | ||
|
||
function test_emergencyTokenTransfer_withTransferRestriction() public { | ||
uint256 amount = 100; | ||
|
||
restrictedMockToken.mint(address(rescuable), amount); | ||
|
||
vm.expectEmit(true, true, true, true); | ||
emit ERC20Rescued(address(this), address(restrictedMockToken), fundsReceiver, amount - 10); | ||
|
||
rescuable.emergencyTokenTransfer(address(restrictedMockToken), amount - 10); | ||
|
||
assertEq(restrictedMockToken.balanceOf(fundsReceiver), amount - 10); | ||
assertEq(restrictedMockToken.balanceOf(address(rescuable)), 10); | ||
|
||
// we don't revert on zero to prevent griefing, so this will just pass but doing nothing | ||
vm.expectEmit(true, true, true, true); | ||
emit ERC20Rescued(address(this), address(restrictedMockToken), fundsReceiver, 0); | ||
|
||
rescuable.emergencyTokenTransfer(address(restrictedMockToken), 10); | ||
assertEq(restrictedMockToken.balanceOf(address(rescuable)), 10); | ||
} | ||
|
||
function test_emergencyEtherTransfer() public { | ||
uint256 amount = 1 ether; | ||
payable(address(rescuable)).transfer(amount); | ||
|
||
vm.expectEmit(true, true, true, true); | ||
emit NativeTokensRescued(address(this), fundsReceiver, amount); | ||
|
||
rescuable.emergencyEtherTransfer(amount); | ||
|
||
assertEq(address(rescuable).balance, 0); | ||
assertEq(fundsReceiver.balance, amount); | ||
} | ||
|
||
function test_emergencyTokenTransferInsufficientBalance_shouldRevert() public { | ||
uint256 amount = 100; | ||
// Not minting any tokens to the contract | ||
vm.expectRevert(); | ||
rescuable.emergencyTokenTransfer(address(mockToken), amount); | ||
} | ||
|
||
function test_emergencyEtherTransferInsufficientBalance_shouldRevert() public { | ||
uint256 amount = 1 ether; | ||
// Not sending any Ether to the contract | ||
vm.expectRevert(bytes('ETH_TRANSFER_FAIL')); | ||
rescuable.emergencyEtherTransfer(amount); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters