ETH Price: $2,360.68 (+7.98%)

Contract Diff Checker

Contract Name:
Multisig

Contract Source Code:

<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

contract Multisig is Ownable {

  /**
   * @notice The type used to track withdrawal requests.
   */
  struct Withdrawal {
    uint256 amount;
    uint256 fee; // in wei (ETH)
    
    // 2 of the 4 potential signers is enough
    address signer1;
    address signer2;
  }

  /**
   * @notice The well-known public crypto contract.  Cannot change.
   */
  IERC20Metadata public crypto;

  /**
   * @notice Upstream wallet that is used to create Withdrawal objects and to move
   *         Eth to back signer wallets so they can keep signing transactions.
   */
  address public upstreamWallet;

  // The full set of all signers, only 2 are needed to complete a withdrawal.
  address public signer1;
  address public signer2;
  address public signer3;
  address public signer4;

  function decimals() external view returns (uint256) {
    return crypto.decimals();
  }

  /**
   * The current limit for each approval account.  For larger withdrawals
   * the limits will be temporarily increased before being reduced back to
   * a smaller/safer limit.  This process requires back office coordination.
   */
  mapping (address => uint256) public limits;

  /**
   * @notice The collection of all pending withdrawals, only one can be active
   *         per wallet at a time.
   */
  mapping (address => Withdrawal) public withdrawals;

  modifier onlyUpstream {
    require(_msgSender() == upstreamWallet, "You are not Upstream.");
    _;
  }

  // The three events to cover the lifecycle of a withdrawal.  Approved happens for
  // each approver, in this contract that will be at least twice.
  event WithdrawalCreated(address indexed who, uint256 quantity, uint256 fee);
  event WithdrawalApproved(address indexed who, address indexed approver, uint256 fee);
  event WithdrawalComplete(address indexed who, uint256 quantity, uint256 fee);
  event WithdrawalCancelled(address indexed who);

  /**
   * @notice Create an instance of this contract.
   *
   * @param cryptoAddress The address of the well-known Crypto Currency contract, cannot be changed after contract creation.
   * @param upstream Initial Upstream wallet used to initiate the withdrawal.
   * @param addr1 The first address that can approve a withdrawal.
   * @param addr2 The second address that can approve a withdrawal.
   * @param addr3 The third address that can approve a withdrawal.
   * @param addr4 The fourth address that can approve a withdrawal.
   * @param limit The approval limit of each signer, in wei.  e.g. 10 USDC (6 digits precision) = 10000000.
   */
  constructor(address cryptoAddress, address upstream, address addr1, address addr2, address addr3, address addr4, uint256 limit) {
    require(cryptoAddress != address(0), "Crypto contract cannot be null");
    require(upstream != address(0), "Upstream address cannot be null");
    require(addr1 != address(0), "Signer 1 cannot be null");
    require(addr2 != address(0), "Signer 2 cannot be null");
    require(addr3 != address(0), "Signer 3 cannot be null");
    require(addr4 != address(0), "Signer 4 cannot be null");
    require(limit != 0, "Approve limit cannot be zero");

    crypto = IERC20Metadata(cryptoAddress);
    upstreamWallet = upstream;

    signer1 = addr1;
    limits[signer1] = limit;

    signer2 = addr2;
    limits[signer2] = limit;
    
    signer3 = addr3;
    limits[signer3] = limit;

    signer4 = addr4;
    limits[signer4] = limit;
  }

  /**
   * @param addr The new address to give the Upstream wallet.
   */
  function setUpstreamWallet(address addr) external onlyOwner
  {
    require(addr != address(0), "Upstream address cannot be null");

    upstreamWallet = addr;
  }

  /**
   * @param addr The new address to give the first approval wallet.
   * @param limit The approval limit for this account.
   *
   * @dev No need to protect against duplicate addresses, the logic of the other functions
   *      prevents a single wallet fully approving a withdrawal.
   */
  function setSigner1(address addr, uint256 limit) external onlyOwner
  {
    require(addr != address(0), "Signer 1 cannot be null");
    signer1 = addr;
    limits[signer1] = limit;
  }

  /**
   * @param addr The new address to give the second approval wallet.
   * @param limit The approval limit for this account.
   */
  function setSigner2(address addr, uint256 limit) external onlyOwner
  {
    require(addr != address(0), "Signer 2 cannot be null");
    signer2 = addr;
    limits[signer2] = limit;
  }

  /**
   * @param addr The new address to give the third approval wallet.
   * @param limit The approval limit for this account.
   */
  function setSigner3(address addr, uint256 limit) external onlyOwner
  {
    require(addr != address(0), "Signer 3 cannot be null");
    signer3 = addr;
    limits[signer3] = limit;
  }

  /**
   * @param addr The new address to give the fourth approval wallet.
   * @param limit The approval limit for this account.
   */
  function setSigner4(address addr, uint256 limit) external onlyOwner
  {
    require(addr != address(0), "Signer 4 cannot be null");
    signer4 = addr;
    limits[signer4] = limit;
  }

  /**
   * @notice Get the withdrawal amount pending for the caller.
   */
  function getAmount() external view returns (uint256) {
    return getAmountFor(_msgSender());
  }

  /**
   * @notice Get the withdrawal amount pending for the specified address.
   */
  function getAmountFor(address payee) public view returns (uint256) {
    require(withdrawals[payee].amount != 0, "No withdrawal pending.");
    Withdrawal memory withdrawal = withdrawals[payee];
    return withdrawal.amount;
  }

  /**
   * @notice Get the withdrawal fee pending for the caller.
   */
  function getFee() external view returns (uint256) {
    return getFeeFor(_msgSender());
  }

  /**
   * @notice Get the withdrawal fee pending for the specified address.
   */
  function getFeeFor(address payee) public view returns (uint256) {
    require(withdrawals[payee].amount != 0, "No withdrawal pending.");
    Withdrawal memory withdrawal = withdrawals[payee];
    return withdrawal.fee;
  }

  /**
   * @notice Get the number of approvals remaining before the withdrawal can complete
   *         for the current wallet.
   */
  function getRemainingConfirmations() external view returns (uint256) {
    return getRemainingConfirmationsFor(_msgSender());
  }

  /**
   * @notice Get the number of approvals remaining before the withdrawal can complete
   *         for the specified wallet.
   */
  function getRemainingConfirmationsFor(address payee) public view returns (uint256) {
    require(withdrawals[payee].amount != 0, "No withdrawal pending.");

    Withdrawal memory withdrawal = withdrawals[payee];
    if (withdrawal.signer1 == address(0)) return 2;
    if (withdrawal.signer2 == address(0)) return 1;
    return 0;
  }

  /**
   * @notice Create a new withdrawal for the payee.  One must not already exist (no overwrite
   *         and no multiple withdrawals in parallel).
   *
   * @param payee The recipient of the withdrawal.
   * @param amount The amount to withdraw, in wei.
   * @param fee The Eth transaction cost (gas Gwei * gas used) to create this withdrawal.
   */
  function createWithdrawal(address payee, uint256 amount, uint256 fee) external onlyUpstream {
    require(withdrawals[payee].amount == 0, "One payment to a payee at a time.");
    require(amount > 0, "Invalid amount.");
    require(fee > 0, "Invalid fee.");

    withdrawals[payee] = Withdrawal(amount, fee, address(0), address(0));

    emit WithdrawalCreated(payee, amount, fee);
  }  

  /**
   * @notice One of the four multisig wallets confirms that this is a valid withdrawal.
   *         Only after two separate multisig wallets confirm will user be able to complete the
   *         withdrawal process.
   *
   * @param payee The address performing the withdrawal.
   * @param fee The Eth transaction cost (gas Gwei * gas used) to confirm this withdrawal.
   */
  function confirm(address payee, uint256 fee) external {
    require(_msgSender() == signer1 || _msgSender() == signer2 ||
            _msgSender() == signer3 || _msgSender() == signer4, "Invalid approver.");
    require(withdrawals[payee].amount != 0, "No withdrawal pending.");
    Withdrawal memory withdrawal = withdrawals[payee];
    require(limits[_msgSender()] >= withdrawal.amount, "Amount above approval limit.");
    require(withdrawal.signer1 == address(0) || withdrawal.signer2 == address(0), "Withdrawal already confirmed.");
    require(fee > 0, "Invalid fee.");

    if (withdrawal.signer1 == address(0)) {
      withdrawals[payee].signer1 = _msgSender();
    } else if (withdrawal.signer1 == _msgSender()) {
      revert("Address already approved this withdrawal.");
    } else if (withdrawal.signer2 == address(0)) {
      withdrawals[payee].signer2 = _msgSender();
    }
    withdrawals[payee].fee = withdrawals[payee].fee + fee;
    
    emit WithdrawalApproved(payee, _msgSender(), fee);
  }

  /**
   * @notice The user completes the withdrawal after 2 multisigs.
   *
   * @dev Note the fee is paid directly by the caller, not subtracted from the amount to protect
   *      the user from excessive fees.
   */
  function withdraw() external payable {
    require(withdrawals[_msgSender()].amount != 0, "No withdrawal pending.");
    Withdrawal memory withdrawal = withdrawals[_msgSender()];
    require(withdrawal.signer1 != address(0) && withdrawal.signer2 != address(0), "Pending confirmation.");
    require(msg.value >= withdrawal.fee, "Insufficient fee.");
    require(crypto.balanceOf(address(this)) >= withdrawal.amount, "Insufficient balance in this contract, please try later.");

    uint256 amount = withdrawal.amount;
    withdrawals[_msgSender()] = Withdrawal(0, 0, address(0), address(0));
    if(!crypto.transfer(_msgSender(), amount))
      revert("Crypto transfer failed");

    emit WithdrawalComplete(_msgSender(), amount, withdrawal.fee);
  }

  /**
   * @notice Cancel a pending withdrawal.  Upstream back-office admin function.
   *
   * @param payee The address of the payee having the withdrawal cancelled.
   */
  function cancel(address payee) external onlyUpstream {
    require(withdrawals[payee].amount != 0, "No withdrawal pending.");

    withdrawals[payee] = Withdrawal(0, 0, address(0), address(0));

    emit WithdrawalCancelled(payee);
  }

  /**
   * @notice Transfer the paid-in Eth fees back out to signers wallets.
   *
   * @param to The address to send the Ether from this contract to.
   * @param amount The amount of Eth to transfer.
   */
  function transfer(address payable to, uint256 amount) external onlyUpstream returns (bool) { 
    require(to != address(0), "Cannot transfer to null");
    require(amount != 0, "Cannot transfer zero");

    to.transfer(amount);

    return true;
  }  
}

<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol)

pragma solidity ^0.8.0;

import "../IERC20.sol";

/**
 * @dev Interface for the optional metadata functions from the ERC20 standard.
 *
 * _Available since v4.1._
 */
interface IERC20Metadata is IERC20 {
    /**
     * @dev Returns the name of the token.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the symbol of the token.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the decimals places of the token.
     */
    function decimals() external view returns (uint8);
}

<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC20 standard as defined in the EIP.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the amount of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the amount of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves `amount` tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 amount) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `from` to `to` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) external returns (bool);
}

<i class='far fa-question-circle text-muted ms-2' data-bs-trigger='hover' data-bs-toggle='tooltip' data-bs-html='true' data-bs-title='Click on the check box to select individual contract to compare. Only 1 contract can be selected from each side.'></i>

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

Please enter a contract address above to load the contract details and source code.

Context size (optional):