Releasable Contract - Withdraw Stuck or Incompatible Tokens From Your Contract

One of the major problems with ERC20 and ERC721 tokens is that they can get stuck or lost, sent to incompatible contracts instead of a wallet. I imagine this is mostly from inexperienced users pasting the wrong address into the "to" field on transfers. Though newer contracts like ERC1155 throw an error if not sent to an ERC1155 compatible contract, there may be no function to release a specific ERC1155 token if it was accidentally sent to the contract.

It really sucks if it is your contract that someone else lost their money to, because you can't send it back and save them from their woes. You don't have to be that guy/gal any more.

Before making this contract, I was using OZ's payment splitter, but I didn't like the immutability. It was set it and forget it, and I just couldn't forget it. I needed something I could update, and honestly, it didn't have to be a payment split. It could be to any address. Better if I could send it directly to its recipient without having to withdraw it first. Saves gas that way.

Another pain point I imagine someone may have is the requirement for a simple contract that can be used to withdraw tokens from itself. Perhaps someone doesn't like ABI calls required to use a Timelock contract and just wants to use etherscan. Maybe they need a way to issue a refund. Maybe they need a way to migrate to a new contract.

To prevent malicious central activity (theft), I've added a few functions to disallow certain tokens from withdrawal and then make that list immutable. That way, this can be used in something like an LP contract for incompatible tokens but disable the release of the sides of the token pair by the admin, keeping investor tokens safe from central manipulation. This can even be done at deployment by populating the __disallow address array and setting __mutable to false.

I've included instructions to make this contract more lightweight. If your contract was not intended to be ERC721 or ERC1155 compatible, then it makes no sense to add function to return stuck tokens for safe transfers. Thus, certain code can be neglected from the main contract and commented out from this one.

Here is an active example:

Note the above example is extremely central. I do not recommend it for any decentralized project.

Below is the code and instructions:

/*
╔═══╗╔═══╗╔╗   ╔═══╗╔═══╗╔═══╗╔═══╗╔══╗ ╔╗   ╔═══╗
║╔═╗║║╔══╝║║   ║╔══╝║╔═╗║║╔═╗║║╔═╗║║╔╗║ ║║   ║╔══╝
║╚═╝║║╚══╗║║   ║╚══╗║║ ║║║╚══╗║║ ║║║╚╝╚╗║║   ║╚══╗
║╔╗╔╝║╔══╝║║ ╔╗║╔══╝║╚═╝║╚══╗║║╚═╝║║╔═╗║║║ ╔╗║╔══╝
║║║╚╗║╚══╗║╚═╝║║╚══╗║╔═╗║║╚═╝║║╔═╗║║╚═╝║║╚═╝║║╚══╗
╚╝╚═╝╚═══╝╚═══╝╚═══╝╚╝ ╚╝╚═══╝╚╝ ╚╝╚═══╝╚═══╝╚═══╝
A People's Treasury(TM) contract.
https://peoplestreasury.com/
*/

/// @title The Releasable Contract v1.0.0
/// @author People's Treasury
/// @notice Allows contract to accept and release network, ERC20, ERC721, and ERC1155 tokens.

// SPDX-License-Identifier: MIT

/*
In order to use this contract to the fullest potential, please follow the below instructions:
0. Start a contract from a template such as a token, LP, or NFT contract. Open Zeppelin's wizard may be helpful to start.
1. Import this contract along with Open Zeppelin's AccessControl, IERC721Receiver, and IERC1155Receiver.
2. Add these to the contract declations: IERC721Receiver, IERC1155Receiver, AccessControl.
3. Add `address[] memory __disallowed, bool __mutable` to the constructor arguments.
4. Add `Releasable(__disallowed, __mutable) payable` to the constructor modifiers.
5. Add the following functions to the contract:
    function allowToken(address token) public onlyRole(DEFAULT_ADMIN_ROLE) {_allowToken(token);}
    function disallowToken(address token) public onlyRole(DEFAULT_ADMIN_ROLE) {_disallowToken(token);}
    function lockAllowToken() public onlyRole(DEFAULT_ADMIN_ROLE) {_lockAllowToken();}
    function lockDisallowToken() public onlyRole(DEFAULT_ADMIN_ROLE) {_lockDisallowToken();}
    function releaseAllETH(address payable account) public onlyRole(DEFAULT_ADMIN_ROLE) {_releaseAllETH(account);}
    function releaseETH(address payable account, uint256 amount) public onlyRole(DEFAULT_ADMIN_ROLE) {_releaseETH(account, amount);}
    function releaseAllERC20(IERC20 token, address account) public onlyRole(DEFAULT_ADMIN_ROLE) {_releaseAllERC20(token, account);}
    function releaseERC20(IERC20 token, address account, uint256 amount) public onlyRole(DEFAULT_ADMIN_ROLE) {_releaseERC20(token, account, amount);}
    function releaseERC721(IERC721 token, address account, uint256 tokenId) public onlyRole(DEFAULT_ADMIN_ROLE) {_releaseERC721(token, account, tokenId);}
    function releaseAllERC1155(IERC1155 token, address account, uint256 tokenId) public onlyRole(DEFAULT_ADMIN_ROLE) {_releaseAllERC1155(token, account, tokenId);}
    function releaseERC1155(IERC1155 token, address account, uint256 tokenId, uint256 amount) public onlyRole(DEFAULT_ADMIN_ROLE) {_releaseERC1155(token, account, tokenId, amount);}
    function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, AccessControl) returns (bool) {return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId);}
6. When deploying, add disallowed token addresses, such as the two sides to a liquidity pair, a test token, or a known investor tokens. Example:  ["0xTEST_TOKEN"]
7. Use the allowToken and disallowToken functions to update the disallowed token list. 
8. To make the disallowed list immutable, use the lockAllowToken and lockDisallowToken functions to prevent subtractions and additions, respectively.
NOTES:
 - To use Ownable instead of access control...
    1) Replace AccessControl with Ownable in the imports and contract declarations.
    2) Replace onlyRole(DEFAULT_ADMIN_ROLE) with OnlyOwner.
    3) Remove the supportsInterface function, since there are no more redundancies requiring an override.
 - There are several other ways to make the disallowed list immutable (to show there is no backdoor for supposedly secure funds):
    1) Set __mutable to false when deploying contract.
    2) Call the lockAllowToken() and lockDisallowToken() functions in the constructor function.
    3) Neglect to add allowToken() and disallowToken() to the contract.
    4) Renounce admin/owner and any applicable roles.
 - To reduce byte size if using this contract for unstucking tokens, remove all reference to ERC1155 (can't get stuck) and IERC721Receiver (can get stuck despite receiver).
    1) Remove imports and contract declarations of IERC721Receiver, IERC1155Receiver in main contract.
    2) Remove releaseAllERC1155, releaseERC1155, and supportsInterface functions in main contract.
    3) Comment out IERC1155.sol import and ReleasedERC1155 event as well as _callOptionalReturnERC1155, _releaseAllERC1155, and _releaseERC1155 functions in this contract.
 - Disallowing the raw network token will not work. Here are some alternatives to disable their release:
    1) Neglect to add the releaseAllETH and releaseETH functions to the contract
    2) Use access control to create a lite admin role for all other releases and revoke admin role.
    3) Employ the use of wrapped tokens.
*/

pragma solidity ^0.8.0; // Solidity must be 0.8.0 or higher.

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; // IERC20 imported to use IERC20 indexed variable.
import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; // IERC721 imported to use IERC721 indexed variable.
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; // IERC1155 imported to use IERC1155 indexed variable.
import "@openzeppelin/contracts/utils/Address.sol"; // Address imported to use address()

abstract contract Releasable {

    using Address for address;

/// @dev Events defined for any contract changes.

    event ReleasedETH(address to, uint256 amount); // Emits event "Released ETH" when network tokens have been released, returning recipient and amount.
    event ReleasedERC20(IERC20 indexed token, address to, uint256 amount);  // Emits event "Released ERC20" when ERC20 tokens have been released, returning token, recipient, and amount.
    event ReleasedERC721(IERC721 indexed token, address to, uint256 tokenId);  // Emits event "Released ERC721" when ERC721 tokens have been released, returning token, recipient, and ID.
    event ReleasedERC1155(IERC1155 indexed token, address to, uint256 tokenId, uint256 amount);  // Emits event "Released ERC1155" when ERC1155 tokens have been released, returning token, recipient, ID, and amount.
    event DisallowToken(address token); // Emits event "Disallow Token" when new token has been added to disallow list.
    event AllowToken(address token); // Emits even "Allow Token" when token has been removed from the allow list.

/// @dev Name and type of constants defined.
    
    mapping(address => bool) private _disallowed; // Stores list of disallowed tokens as a mapping to a boolean.
    bool private _allowUnlocked; // Stores ability to make subtractions to the disallowed token list as a boolean.
    bool private _disallowUnlocked; // Stores ability to make additions to the disallowed token list as a boolean.

/// @dev Constructor includes payable modifier.

    constructor(address[] memory __disallowed, bool __mutable) payable {
        _allowUnlocked = __mutable; // Recommended: false, unless testing.
        _disallowUnlocked = __mutable; // Recommended: false, unless testing.
        for (uint256 i = 0; i < __disallowed.length; i++) {
            _disallowToken(__disallowed[i]); // Recommended: liquidity pair sides, test tokens, investor tokens, etc.
        }
    }

/// @dev External payable function allows contract to receive ETH.

    receive() external payable virtual {}

/// @dev Public view functions allow privately stored constants to be interfaced.

	function disallowed(address token) public view virtual returns (bool) { // Function enables public interface for boolean if token is on disallow list.
        return _disallowed[token]; // Returns true if token is disallowed, false if it is allowed.
    }

    function allowUnlocked() public view virtual returns (bool) { // Function enables public interface for boolean if disallow list is locked from purge.
        return _allowUnlocked; // Returns true if tokens from disallow list can be removed.
    }

    function disallowUnlocked() public view virtual returns (bool) { // Function enables public interface for boolean if disallow list is locked from adding.
        return _disallowUnlocked; // Returns true if tokens from disallow list can be added.
    }

/// @dev Internal functions to require token be allowed or disallowed.

    function _requireTokenAllowed(address token) internal view virtual { // Used in the _disallowToken(token) private function.
        require(_disallowed[token] != true, "Releasable: token must not be disallowed"); // Throws the call if the token is already disallowed.
    }

    function _requireTokenDisallowed(address token) internal view virtual { // Used in the _allowToken(token) private function.
        require(_disallowed[token] == true, "Releasable: token must not be allowed"); // Throws the call if the token is already allowed.
    }

    function _requireAllowUnlocked() internal view virtual { // Used in the _allowToken(token) private function.
        require(_allowUnlocked == true, "Releasable: disallow list locked from allowing new tokens"); // Throws the call if disallow list is locked from subtractions.
    }
    
    function _requireDisallowUnlocked() internal view virtual { // Used in the _disallowToken(token) private function.
        require(_disallowUnlocked == true, "Releasable: disallow list locked from disallowing new tokens");  // Throws the call if disallow list is locked from additions.
    }

/// @dev Internal functions allow external calls to NFT's for transfering.

    function _callOptionalReturnERC721(IERC721 token, bytes memory data) private { // Used in the _releaseERC721(..) private function to execute the transfer.
        bytes memory returndata = address(token).functionCall(data, "Releasable: low-level call failed"); // Executes transfer function. Throws the call if ERC721 is not responsive.
        if (returndata.length > 0) {
            require(abi.decode(returndata, (bool)), "Releasable: ERC721 operation did not succeed"); // Sends back an error if ERC721 returns no data.
        }
    }
    
    function _callOptionalReturnERC1155(IERC1155 token, bytes memory data) private { // Used in the _releaseERC1155(..) private function to execute the transfer.
        bytes memory returndata = address(token).functionCall(data, "Releasable: low-level call failed"); // Executes transfer function. Throws the call if ERC1155 is not responsive.
        if (returndata.length > 0) {
            require(abi.decode(returndata, (bool)), "Releasable: ERC1155 operation did not succeed"); // Sends back an error if ERC1155 returns no data.
        }
    }

/// @dev Internal virtual functions update the disallowed list of tokens.

    function _allowToken(address token) internal virtual { // Subtracts tokens from disallow list if subtractions are unlocked and token isn't already allowed.
        _requireAllowUnlocked(); // Makes sure subtractions are unlocked.
        _requireTokenDisallowed(token); // Makes sure token isn't already allowed.
        _disallowed[token] = false; // Sets the disallow boolean mapping to false, indicating the token is no longer disallowed.
        emit AllowToken(token); // Emits the "Allow Token" event to the blockchain.
    }
    
    function _disallowToken(address token) internal virtual { // Adds tokens to disallow list if additions are unlocked and token isn't already disallowed.
        _requireDisallowUnlocked(); // Makes sure additions are unlocked.
        _requireTokenAllowed(token); // Makes sure token isn't already disallowed.
        _disallowed[token] = true; // Sets the disallow boolean mapping to true, indicating the token is now disallowed.
        emit DisallowToken(token); // Emits the "Disllow Token" event to the blockchain.
    }

    function _lockAllowToken() internal virtual { // Locks further subtractions from disallow list. Immutable once performed.
        _allowUnlocked = false; // Sets boolean for disallow list subtractions to false; tokens can no longer be removed from disallow list.
    }
    
    function _lockDisallowToken() internal virtual {// Locks further subtractions from disallow list. Immutable once performed.
        _disallowUnlocked = false; // Sets boolean for disallow list additions to false; tokens can no longer be added to disallow list.
    }

/// @dev Internal virtual functions perform the requested contract updates and emit the events to the blockchain.

    function _releaseAllETH(address payable account) internal virtual { // Releases all network tokens from contract.
		uint256 payment = address(this).balance; // Sets payment to the contract's network token balance.
        _releaseETH(account, payment); // Calls the _releaseETH function with total balance.
    }

    function _releaseETH(address payable account, uint256 payment) internal virtual { // Releases specified amount of network tokens from contract.
		uint256 totalBalance = address(this).balance;  // Gets the contract's total network token balance
        require(totalBalance != 0, "Releasable: no network tokens to release"); // Throws call if there are no network tokens to release.
        require(totalBalance >= payment, "Releasable: not enough network tokens to release"); // Throws call if the requested amount is greater than the balance.
        Address.sendValue(account, payment); // Sends the network tokens securely via the the sendValue call.
        emit ReleasedETH(account, payment);  // Emits the "Released E T H" event to the blockchain.
    }

    function _releaseAllERC20(IERC20 token, address account) internal virtual { // Releases all of a specified ERC20 token from contract.
		uint256 payment = token.balanceOf(address(this)); // Sets payment to the contract's ERC20 token balance.
        _releaseERC20(token, account, payment); // Calls the _releaseERC20 function with total balance.
    }
    
    function _releaseERC20(IERC20 token, address account, uint256 payment) internal virtual { // Releases specified amount of ERC20 tokens from contract.
        _requireTokenAllowed(address(token)); // Throws call if token is on the disallow list.
		uint256 totalBalance = token.balanceOf(address(this)); // Gets the contract's total ERC20 balance
        require(totalBalance != 0, "Releasable: no ERC20 tokens to release"); // Throws call if there are no ERC20 tokens to release.
        require(totalBalance >= payment, "Releasable: not enough ERC20 tokens to release"); // Throws call if the requested amount is greater than the balance.
        SafeERC20.safeTransfer(token, account, payment); // Sends the ERC20 tokens securely via the the safeTransfer call.
        emit ReleasedERC20(token, account, payment); // Emits the "Released ERC20" event to the blockchain.
    }

    function _releaseERC721(IERC721 token, address account, uint256 tokenId) internal virtual { // Releases specified ERC721 NFT from contract.
        _requireTokenAllowed(address(token)); // Throws call if token is on the disallow list.
        address owner = token.ownerOf(tokenId); // Gets the owner of the specified ERC721 NFT.
        require(owner == address(this), "Releasable: contract is not owner of ERC721 NFT"); // Throws the call if the owner of the NFT is not the contract.
        _callOptionalReturnERC721(token, abi.encodeWithSelector(token.transferFrom.selector, address(this), account, tokenId)); // Sends the ERC721 via transferFrom abi call.
        // Unable to use "safeTransferFrom" due to "not unique after argument-dependent lookup" error.
        emit ReleasedERC721(token, account, tokenId); // Emits the "Released ERC721" event to the blockchain.
    }

    function _releaseAllERC1155(IERC1155 token, address account, uint256 tokenId) internal virtual { // Releases all of a specified ERC1155 NFT's from contract.
        uint256 payment = token.balanceOf(address(this), tokenId); // Sets payment to the contract's ERC1155 NFT balance.
        _releaseERC1155(token, account, tokenId, payment); // Calls the _releaseERC1155 function with total NFT balance.
    }

    function _releaseERC1155(IERC1155 token, address account, uint256 tokenId, uint256 payment) internal virtual { // Releases specified ERC1155 NFT's from contract.
        _requireTokenAllowed(address(token)); // Throws call if token is on the disallow list.
        uint256 totalBalance = token.balanceOf(address(this), tokenId); // Gets the contract's total ERC1155 NFT balance
        require(totalBalance != 0, "Releasable: no ERC1155 NFTs to release"); // Throws call if there are no ERC1155 NFT's to release.
        require(totalBalance >= payment, "Releasable: not enough ERC1155 NFTs to release"); // Throws call if the requested amount is greater than the balance.
        _callOptionalReturnERC1155(token, abi.encodeWithSelector(token.safeTransferFrom.selector, address(this), account, tokenId, payment, "")); // Sends the ERC1155 via safeTransferFrom abi call.
        emit ReleasedERC1155(token, account, tokenId, payment); // Emits the "Released ERC155" event to the blockchain.
    }

}

Let me know if there are any major security holes in this method. It's decidedly central protected by access control, so I am more concerned with outside attacks.

Created a lightweight version and an example bare bones ERC20 to go with it. The ReleasableLite contract should work for any contract that isn't set up to receive ERC1155 tokens, otherwise you need the full version.

Here is a live testnet version:

I was able to use remix to make this, then hardhat to verify since remix isn't great for that at the moment.

Here is the ReleasableLite contract:

/*
╔═══╗╔═══╗╔╗   ╔═══╗╔═══╗╔═══╗╔═══╗╔══╗ ╔╗   ╔═══╗
║╔═╗║║╔══╝║║   ║╔══╝║╔═╗║║╔═╗║║╔═╗║║╔╗║ ║║   ║╔══╝
║╚═╝║║╚══╗║║   ║╚══╗║║ ║║║╚══╗║║ ║║║╚╝╚╗║║   ║╚══╗
║╔╗╔╝║╔══╝║║ ╔╗║╔══╝║╚═╝║╚══╗║║╚═╝║║╔═╗║║║ ╔╗║╔══╝
║║║╚╗║╚══╗║╚═╝║║╚══╗║╔═╗║║╚═╝║║╔═╗║║╚═╝║║╚═╝║║╚══╗
╚╝╚═╝╚═══╝╚═══╝╚═══╝╚╝ ╚╝╚═══╝╚╝ ╚╝╚═══╝╚═══╝╚═══╝
A People's Treasury(TM) contract.
https://peoplestreasury.com/
*/

/// @title The Releasable Lite Contract v1.0.0
/// @author People's Treasury
/// @notice Allows contract to release network, ERC20, and ERC721 tokens.

// SPDX-License-Identifier: MIT

/*
In order to use this contract to the fullest potential, please follow the below instructions:
0. Start a contract from a template such as a token, LP, or NFT contract. Open Zeppelin's wizard may be helpful to start.
1. Import this contract along with Open Zeppelin's AccessControl.
2. Add AccessControl to the contract declarations.
3. Add `address[] memory __disallowed, bool __mutable` to the constructor arguments.
4. Add `ReleasableLite(__disallowed, __mutable) payable` to the constructor modifiers.
5. Add the following functions to the contract:
    function allowToken(address token) public onlyRole(DEFAULT_ADMIN_ROLE) {_allowToken(token);}
    function disallowToken(address token) public onlyRole(DEFAULT_ADMIN_ROLE) {_disallowToken(token);}
    function lockAllowToken() public onlyRole(DEFAULT_ADMIN_ROLE) {_lockAllowToken();}
    function lockDisallowToken() public onlyRole(DEFAULT_ADMIN_ROLE) {_lockDisallowToken();}
    function releaseAllETH(address payable account) public onlyRole(DEFAULT_ADMIN_ROLE) {_releaseAllETH(account);}
    function releaseETH(address payable account, uint256 amount) public onlyRole(DEFAULT_ADMIN_ROLE) {_releaseETH(account, amount);}
    function releaseAllERC20(IERC20 token, address account) public onlyRole(DEFAULT_ADMIN_ROLE) {_releaseAllERC20(token, account);}
    function releaseERC20(IERC20 token, address account, uint256 amount) public onlyRole(DEFAULT_ADMIN_ROLE) {_releaseERC20(token, account, amount);}
    function releaseERC721(IERC721 token, address account, uint256 tokenId) public onlyRole(DEFAULT_ADMIN_ROLE) {_releaseERC721(token, account, tokenId);}
6. When deploying, add disallowed token addresses, such as the two sides to a liquidity pair, a test token, or a known investor tokens. Example:  ["0xTEST_TOKEN"] or []
7. Use the allowToken and disallowToken functions to update the disallowed token list. 
8. To make the disallowed list immutable, use the lockAllowToken and lockDisallowToken functions to prevent subtractions and additions, respectively.
NOTES:
 - To use Ownable instead of access control...
    1) Replace AccessControl with Ownable in the imports and contract declarations.
    2) Replace onlyRole(DEFAULT_ADMIN_ROLE) with OnlyOwner.
    3) Remove the supportsInterface function, since there are no more redundancies requiring an override.
 - There are several other ways to make the disallowed list immutable (to show there is no backdoor for supposedly secure funds):
    1) Set __mutable to false when deploying contract.
    2) Call the lockAllowToken() and lockDisallowToken() functions in the constructor function.
    3) Neglect to add allowToken() and disallowToken() to the contract.
    4) Renounce admin/owner and any applicable roles.
 - This is the Lite version of the contract which does not accept ERC1155 tokens, since they they can't get stuck in an incompatible contract.
 - Disallowing the raw network token will not work. Here are some alternatives to disable their release:
    1) Neglect to add the releaseAllETH and releaseETH functions to the contract
    2) Use access control to create a lite admin role for all other releases and revoke admin role.
    3) Employ the use of wrapped tokens.
*/

pragma solidity ^0.8.0; // Solidity must be 0.8.0 or higher.

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; // IERC20 imported to use IERC20 indexed variable.
import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; // IERC721 imported to use IERC721 indexed variable.
import "@openzeppelin/contracts/utils/Address.sol"; // Address imported to use address()

abstract contract ReleasableLite {

    using Address for address;

/// @dev Events defined for any contract changes.

    event ReleasedETH(address to, uint256 amount); // Emits event "Released ETH" when network tokens have been released, returning recipient and amount.
    event ReleasedERC20(IERC20 indexed token, address to, uint256 amount);  // Emits event "Released ERC20" when ERC20 tokens have been released, returning token, recipient, and amount.
    event ReleasedERC721(IERC721 indexed token, address to, uint256 tokenId);  // Emits event "Released ERC721" when ERC721 tokens have been released, returning token, recipient, and ID.
    event DisallowToken(address token); // Emits event "Disallow Token" when new token has been added to disallow list.
    event AllowToken(address token); // Emits even "Allow Token" when token has been removed from the allow list.

/// @dev Name and type of constants defined.
    
    mapping(address => bool) private _disallowed; // Stores list of disallowed tokens as a mapping to a boolean.
    bool private _allowUnlocked; // Stores ability to make subtractions to the disallowed token list as a boolean.
    bool private _disallowUnlocked; // Stores ability to make additions to the disallowed token list as a boolean.

/// @dev Constructor includes payable modifier.

    constructor(address[] memory __disallowed, bool __mutable) payable {
        _allowUnlocked = __mutable; // Recommended: false, unless testing.
        _disallowUnlocked = __mutable; // Recommended: false, unless testing.
        for (uint256 i = 0; i < __disallowed.length; i++) {
            _disallowToken(__disallowed[i]); // Recommended: liquidity pair sides, test tokens, investor tokens, etc.
        }
    }

/// @dev External payable function allows contract to receive ETH.

    receive() external payable virtual {}

/// @dev Public view functions allow privately stored constants to be interfaced.

	function disallowed(address token) public view virtual returns (bool) { // Function enables public interface for boolean if token is on disallow list.
        return _disallowed[token]; // Returns true if token is disallowed, false if it is allowed.
    }

    function allowUnlocked() public view virtual returns (bool) { // Function enables public interface for boolean if disallow list is locked from purge.
        return _allowUnlocked; // Returns true if tokens from disallow list can be removed.
    }

    function disallowUnlocked() public view virtual returns (bool) { // Function enables public interface for boolean if disallow list is locked from adding.
        return _disallowUnlocked; // Returns true if tokens from disallow list can be added.
    }

/// @dev Internal functions to require token be allowed or disallowed.

    function _requireTokenAllowed(address token) internal view virtual { // Used in the _disallowToken(token) private function.
        require(_disallowed[token] != true, "Releasable: token must not be disallowed"); // Throws the call if the token is already disallowed.
    }

    function _requireTokenDisallowed(address token) internal view virtual { // Used in the _allowToken(token) private function.
        require(_disallowed[token] == true, "Releasable: token must not be allowed"); // Throws the call if the token is already allowed.
    }

    function _requireAllowUnlocked() internal view virtual { // Used in the _allowToken(token) private function.
        require(_allowUnlocked == true, "Releasable: disallow list locked from allowing new tokens"); // Throws the call if disallow list is locked from subtractions.
    }
    
    function _requireDisallowUnlocked() internal view virtual { // Used in the _disallowToken(token) private function.
        require(_disallowUnlocked == true, "Releasable: disallow list locked from disallowing new tokens");  // Throws the call if disallow list is locked from additions.
    }

/// @dev Internal functions allow external calls to NFT's for transfering.

    function _callOptionalReturnERC721(IERC721 token, bytes memory data) private { // Used in the _releaseERC721(..) private function to execute the transfer.
        bytes memory returndata = address(token).functionCall(data, "Releasable: low-level call failed"); // Executes transfer function. Throws the call if ERC721 is not responsive.
        if (returndata.length > 0) {
            require(abi.decode(returndata, (bool)), "Releasable: ERC721 operation did not succeed"); // Sends back an error if ERC721 returns no data.
        }
    }

/// @dev Internal virtual functions update the disallowed list of tokens.

    function _allowToken(address token) internal virtual { // Subtracts tokens from disallow list if subtractions are unlocked and token isn't already allowed.
        _requireAllowUnlocked(); // Makes sure subtractions are unlocked.
        _requireTokenDisallowed(token); // Makes sure token isn't already allowed.
        _disallowed[token] = false; // Sets the disallow boolean mapping to false, indicating the token is no longer disallowed.
        emit AllowToken(token); // Emits the "Allow Token" event to the blockchain.
    }
    
    function _disallowToken(address token) internal virtual { // Adds tokens to disallow list if additions are unlocked and token isn't already disallowed.
        _requireDisallowUnlocked(); // Makes sure additions are unlocked.
        _requireTokenAllowed(token); // Makes sure token isn't already disallowed.
        _disallowed[token] = true; // Sets the disallow boolean mapping to true, indicating the token is now disallowed.
        emit DisallowToken(token); // Emits the "Disllow Token" event to the blockchain.
    }

    function _lockAllowToken() internal virtual { // Locks further subtractions from disallow list. Immutable once performed.
        _allowUnlocked = false; // Sets boolean for disallow list subtractions to false; tokens can no longer be removed from disallow list.
    }
    
    function _lockDisallowToken() internal virtual {// Locks further subtractions from disallow list. Immutable once performed.
        _disallowUnlocked = false; // Sets boolean for disallow list additions to false; tokens can no longer be added to disallow list.
    }

/// @dev Internal virtual functions perform the requested contract updates and emit the events to the blockchain.

    function _releaseAllETH(address payable account) internal virtual { // Releases all network tokens from contract.
		uint256 payment = address(this).balance; // Sets payment to the contract's network token balance.
        _releaseETH(account, payment); // Calls the _releaseETH function with total balance.
    }

    function _releaseETH(address payable account, uint256 payment) internal virtual { // Releases specified amount of network tokens from contract.
		uint256 totalBalance = address(this).balance;  // Gets the contract's total network token balance
        require(totalBalance != 0, "Releasable: no network tokens to release"); // Throws call if there are no network tokens to release.
        require(totalBalance >= payment, "Releasable: not enough network tokens to release"); // Throws call if the requested amount is greater than the balance.
        Address.sendValue(account, payment); // Sends the network tokens securely via the the sendValue call.
        emit ReleasedETH(account, payment);  // Emits the "Released E T H" event to the blockchain.
    }

    function _releaseAllERC20(IERC20 token, address account) internal virtual { // Releases all of a specified ERC20 token from contract.
		uint256 payment = token.balanceOf(address(this)); // Sets payment to the contract's ERC20 token balance.
        _releaseERC20(token, account, payment); // Calls the _releaseERC20 function with total balance.
    }
    
    function _releaseERC20(IERC20 token, address account, uint256 payment) internal virtual { // Releases specified amount of ERC20 tokens from contract.
        _requireTokenAllowed(address(token)); // Throws call if token is on the disallow list.
		uint256 totalBalance = token.balanceOf(address(this)); // Gets the contract's total ERC20 balance
        require(totalBalance != 0, "Releasable: no ERC20 tokens to release"); // Throws call if there are no ERC20 tokens to release.
        require(totalBalance >= payment, "Releasable: not enough ERC20 tokens to release"); // Throws call if the requested amount is greater than the balance.
        SafeERC20.safeTransfer(token, account, payment); // Sends the ERC20 tokens securely via the the safeTransfer call.
        emit ReleasedERC20(token, account, payment); // Emits the "Released ERC20" event to the blockchain.
    }

    function _releaseERC721(IERC721 token, address account, uint256 tokenId) internal virtual { // Releases specified ERC721 NFT from contract.
        _requireTokenAllowed(address(token)); // Throws call if token is on the disallow list.
        address owner = token.ownerOf(tokenId); // Gets the owner of the specified ERC721 NFT.
        require(owner == address(this), "Releasable: contract is not owner of ERC721 NFT"); // Throws the call if the owner of the NFT is not the contract.
        _callOptionalReturnERC721(token, abi.encodeWithSelector(token.transferFrom.selector, address(this), account, tokenId)); // Sends the ERC721 via transferFrom abi call.
        // Unable to use "safeTransferFrom" due to "not unique after argument-dependent lookup" error.
        emit ReleasedERC721(token, account, tokenId); // Emits the "Released ERC721" event to the blockchain.
    }

}

Here is the token contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "./ReleasableLite.sol";

contract UnstuckToken is ERC20, AccessControl, ReleasableLite {
    constructor(
        string memory __name,
        string memory __symbol,
        address[] memory __disallowed,
        bool __mutable
    )
    ERC20(__name, __symbol)
    ReleasableLite(__disallowed, __mutable)
    {
        _mint(msg.sender, 1 * 10 ** decimals());
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    function allowToken(address token) public onlyRole(DEFAULT_ADMIN_ROLE) {
        _allowToken(token);
    }

    function disallowToken(address token) public onlyRole(DEFAULT_ADMIN_ROLE) {
        _disallowToken(token);
    }

    function lockAllowToken() public onlyRole(DEFAULT_ADMIN_ROLE) {
        _lockAllowToken();
    }

    function lockDisallowToken() public onlyRole(DEFAULT_ADMIN_ROLE) {
        _lockDisallowToken();
    }
    
    function releaseAllETH(address payable account) public onlyRole(DEFAULT_ADMIN_ROLE) {
        _releaseAllETH(account);
    }
    
    function releaseETH(address payable account, uint256 amount) public onlyRole(DEFAULT_ADMIN_ROLE) {
        _releaseETH(account, amount);
    }

    function releaseAllERC20(IERC20 token, address account) public onlyRole(DEFAULT_ADMIN_ROLE) {
        _releaseAllERC20(token, account);
    }
    
    function releaseERC20(IERC20 token, address account, uint256 amount) public onlyRole(DEFAULT_ADMIN_ROLE) {
        _releaseERC20(token, account, amount);
    }

    function releaseERC721(IERC721 token, address account, uint256 tokenId) public onlyRole(DEFAULT_ADMIN_ROLE) {
        _releaseERC721(token, account, tokenId);
    }

}

The arguments used to launch/verify:

module.exports = [
  "Unstuck Token",
  "UNSTUCK",
  [],
  true
];

My aim here was a barebones example. If the contract is being used to store tokens like in the case of a liquidity pair or masterchef, the disallow list functions deserve a closer look. Maybe you don't include the lock functions. Maybe you don't include any disallow list functions. Maybe you use access control more extensively with a multisig. Up to you and your team/community to determine what is the safest for your investors.


Post your wallet if you want admin access to test it out. Subscribe to this thread so you can do the same for the next.