0

I found a smart contract which performs a Safe Remote Purchase. The currency which is used for the payment is ETH. Here is the code:

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

pragma solidity ^0.8.0;

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

/**
 * @dev Contract module which allows children to implement an emergency stop
 * mechanism that can be triggered by an authorized account.
 *
 * This module is used through inheritance. It will make available the
 * modifiers `whenNotPaused` and `whenPaused`, which can be applied to
 * the functions of your contract. Note that they will not be pausable by
 * simply including this module, only once the modifiers are put in place.
 */
abstract contract Pausable is Context {
    /**
     * @dev Emitted when the pause is triggered by `account`.
     */
    event Paused(address account);

    /**
     * @dev Emitted when the pause is lifted by `account`.
     */
    event Unpaused(address account);

    bool private _paused;

    /**
     * @dev Initializes the contract in unpaused state.
     */
    constructor() {
        _paused = false;
    }

    /**
     * @dev Modifier to make a function callable only when the contract is not paused.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    modifier whenNotPaused() {
        _requireNotPaused();
        _;
    }

    /**
     * @dev Modifier to make a function callable only when the contract is paused.
     *
     * Requirements:
     *
     * - The contract must be paused.
     */
    modifier whenPaused() {
        _requirePaused();
        _;
    }

    /**
     * @dev Returns true if the contract is paused, and false otherwise.
     */
    function paused() public view virtual returns (bool) {
        return _paused;
    }

    /**
     * @dev Throws if the contract is paused.
     */
    function _requireNotPaused() internal view virtual {
        require(!paused(), "Pausable: paused");
    }

    /**
     * @dev Throws if the contract is not paused.
     */
    function _requirePaused() internal view virtual {
        require(paused(), "Pausable: not paused");
    }

    /**
     * @dev Triggers stopped state.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    function _pause() internal virtual whenNotPaused {
        _paused = true;
        emit Paused(_msgSender());
    }

    /**
     * @dev Returns to normal state.
     *
     * Requirements:
     *
     * - The contract must be paused.
     */
    function _unpause() internal virtual whenPaused {
        _paused = false;
        emit Unpaused(_msgSender());
    }
}

My goal is to adapt the code so that it runs with a specific ERC20 token which has already been deployed rather than ETH. I believe I could achieve this goal only if I recreate the token and I import it to the smart contract and deploy them simultaneously.

Would you be able to suggest a smart and elegant way to allow payments with an (already deployed) ERC20 token instead of ETH?

1 Answer 1

1

You don't need to redeploy the token.

I modified the contract to allow purchases with a generic ERC20, specified by the seller in the constructor.

pragma solidity ^0.8.11;

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract PurchaseAgreement {
    using SafeERC20 for IERC20;

    uint256 public immutable value;
    address public immutable token;
    address public immutable seller;
    address public buyer;
    
    enum State { Created, Locked, Release, Inactive }
    State public state;

    constructor(address _token, uint256 _value) {
        seller = msg.sender;
        token = _token;
        value = _value;

        IERC20(_token).safeTransferFrom(msg.sender, address(this), 2*_value);
    }

    /// The function cannot be called at the current state.
    error InvalidState();
    ///  Ony the buyer can call this function
    error OnlyBuyer();
    ///  Ony the seller can call this function
    error OnlySeller();

    modifier inState(State state_) {
        if (state != state_) {
            revert InvalidState();
        }
        _;
    }

    modifier onlyBuyer() {
        if (msg.sender != buyer) {
            revert OnlyBuyer();
        }
        _;
    }

    modifier onlySeller() {
        if (msg.sender != seller) {
            revert OnlySeller();
        }
        _;
    }

    function confirmPurchase() external inState(State.Created) {
        IERC20(token).safeTransferFrom(msg.sender, address(this), 2*value);
        buyer = msg.sender;
        state = State.Locked;
    }

    function confirmReceived() external onlyBuyer inState(State.Locked) {
        state = State.Release;
        IERC20(token).safeTransfer(buyer, value);
    }

    function paySeller() external onlySeller inState(State.Release) {
        state = State.Inactive;
        IERC20(token).safeTransfer(seller, 3*value);
    }

    function abort() external onlySeller inState(State.Created) {
        state = State.Inactive;
        uint256 _balance = IERC20(token).balanceOf(address(this));
        IERC20(token).safeTransfer(seller, _balance);
    }
}

Before they need to pay the tokens, both seller and buyer need to approve the contract as spender. In the case of the seller, you can predict the address to use (see this: How is the address of an Ethereum contract computed?).

By the way, this contract is a little inefficient, since for every purchase the seller needs to deploy a new one and make the approval. A better way would be having a more complex contract that allows to be reused (also in parallel). If you google around you may find more efficient examples.

1
  • Awesome! Thank you very much! this is really helpful!! :) Commented Dec 2, 2022 at 8:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.