Smart Contract Escrow Service Solidity, JavaScript

👤 Sharing: AI
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// Smart Contract for a Simple Escrow Service
contract Escrow {

    // State variables
    address payable public seller; // Address of the seller
    address payable public buyer; // Address of the buyer
    address public arbitrator; // Address of the arbitrator (optional, for disputes)
    uint public amount; // Amount to be held in escrow (in Wei)
    bool public fundsDeposited; // Flag to indicate if funds are deposited
    bool public fundsReleased; // Flag to indicate if funds are released
    bool public disputeOpened;  // Flag to indicate a dispute has been opened

    // Events (to track important actions)
    event FundsDeposited(address indexed from, uint amount);
    event FundsReleased(address indexed to, uint amount);
    event DisputeOpened(address indexed by, string reason);
    event DisputeResolved(address indexed by, address indexed recipient, uint amount, string resolution);
    event EscrowClosed();



    // Constructor - sets up the escrow parameters
    constructor(address payable _seller, address payable _buyer, address _arbitrator, uint _amount) {
        require(_seller != address(0), "Seller address cannot be zero.");
        require(_buyer != address(0), "Buyer address cannot be zero.");
        require(_amount > 0, "Amount must be greater than zero.");

        seller = _seller;
        buyer = _buyer;
        arbitrator = _arbitrator;
        amount = _amount;
        fundsDeposited = false;
        fundsReleased = false;
        disputeOpened = false;
    }

    // Modifier to ensure only the buyer can call a function
    modifier onlyBuyer() {
        require(msg.sender == buyer, "Only the buyer can call this function.");
        _;
    }

    // Modifier to ensure only the seller can call a function
    modifier onlySeller() {
        require(msg.sender == seller, "Only the seller can call this function.");
        _;
    }

    // Modifier to ensure only the arbitrator can call a function
    modifier onlyArbitrator() {
        require(msg.sender == arbitrator, "Only the arbitrator can call this function.");
        require(arbitrator != address(0), "Arbitrator not set."); //Ensure arbitrator is actually set
        _;
    }

    // Modifier to ensure funds have been deposited
    modifier fundsMustBeDeposited() {
        require(fundsDeposited, "Funds must be deposited first.");
        _;
    }

    // Modifier to ensure no dispute has been opened
    modifier noDisputeOpened() {
        require(!disputeOpened, "A dispute is currently open.");
        _;
    }

    // Modifier to ensure dispute is currently opened
    modifier disputeMustBeOpened() {
        require(disputeOpened, "No dispute is currently open.");
        _;
    }


    // Function for the buyer to deposit funds into the escrow
    function depositFunds() external payable onlyBuyer noDisputeOpened {
        require(msg.value == amount, "Incorrect amount sent.  Please send the exact escrow amount.");
        require(!fundsDeposited, "Funds have already been deposited.");

        fundsDeposited = true;
        emit FundsDeposited(msg.sender, amount);
    }

    // Function for the seller to release funds to the buyer
    function releaseFunds() external onlySeller fundsMustBeDeposited noDisputeOpened {
        require(!fundsReleased, "Funds have already been released.");

        fundsReleased = true;
        (bool success, ) = buyer.call{value: amount}(""); // Transfer funds to the buyer
        require(success, "Transfer failed.");

        emit FundsReleased(buyer, amount);
        emit EscrowClosed();
    }

    // Function for the buyer to open a dispute
    function openDispute(string calldata _reason) external onlyBuyer fundsMustBeDeposited noDisputeOpened {
        disputeOpened = true;
        emit DisputeOpened(msg.sender, _reason);
    }

    // Function for the arbitrator to resolve the dispute
    function resolveDispute(address payable _recipient, uint _amount, string calldata _resolution) external onlyArbitrator disputeMustBeOpened {
        require(_recipient == seller || _recipient == buyer, "Recipient must be the seller or the buyer.");
        require(_amount <= amount, "Amount to be sent cannot exceed the escrow amount.");

        disputeOpened = false;
        fundsReleased = true; //Consider dispute being final action

        (bool success, ) = _recipient.call{value: _amount}(""); // Transfer funds to the recipient
        require(success, "Transfer failed.");

        // Return the remaining funds (if any) to the buyer (or the seller if they were the recipient).
        if (_recipient == seller && amount - _amount > 0) {
             (bool success2, ) = buyer.call{value: amount - _amount}("");
             require(success2, "Transfer of remaining funds to buyer failed.");
        } else if (_recipient == buyer && amount - _amount > 0) {
             (bool success2, ) = seller.call{value: amount - _amount}("");
             require(success2, "Transfer of remaining funds to seller failed.");
        }

        emit DisputeResolved(msg.sender, _recipient, _amount, _resolution);
        emit EscrowClosed();
    }

    // Function to allow either the buyer or seller to cancel the escrow if funds have not been deposited.
    function cancelEscrow() external {
        require(!fundsDeposited, "Cannot cancel. Funds already deposited.");
        require(msg.sender == buyer || msg.sender == seller, "Only buyer or seller can cancel.");

        emit EscrowClosed();
        selfdestruct(payable(msg.sender));  // Refund gas.  Dangerous in a production environment.
    }
}
```

```javascript
// Example JavaScript interaction using ethers.js (requires a local Ethereum node like Ganache)
const { ethers } = require("ethers");

// Replace with your contract address and ABI
const contractAddress = "YOUR_CONTRACT_ADDRESS";  //After deployment
const contractABI = [
  // ABI generated from the Solidity contract (copy and paste from Remix or similar)
  {
    "inputs": [
      {
        "internalType": "address payable",
        "name": "_seller",
        "type": "address"
      },
      {
        "internalType": "address payable",
        "name": "_buyer",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_arbitrator",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "_amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "by",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "resolution",
        "type": "string"
      }
    ],
    "name": "DisputeResolved",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FundsDeposited",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FundsReleased",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "cancelEscrow",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "depositFunds",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "openDispute",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "releaseFunds",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address payable",
        "name": "_recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "_amount",
        "type": "uint256"
      },
      {
        "internalType": "string",
        "name": "_resolution",
        "type": "string"
      }
    ],
    "name": "resolveDispute",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "amount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "arbitrator",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "buyer",
    "outputs": [
      {
        "internalType": "address payable",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "disputeOpened",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "fundsDeposited",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "fundsReleased",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "seller",
    "outputs": [
      {
        "internalType": "address payable",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
];

// Configuration - Adjust these values
const providerURL = "http://127.0.0.1:7545"; // Ganache default
const buyerPrivateKey = "YOUR_BUYER_PRIVATE_KEY";
const sellerPrivateKey = "YOUR_SELLER_PRIVATE_KEY";
const arbitratorPrivateKey = "YOUR_ARBITRATOR_PRIVATE_KEY"; //optional, if using arbitrator functionality

// Create a provider and wallets
const provider = new ethers.providers.JsonRpcProvider(providerURL);
const buyerWallet = new ethers.Wallet(buyerPrivateKey, provider);
const sellerWallet = new ethers.Wallet(sellerPrivateKey, provider);
const arbitratorWallet = new ethers.Wallet(arbitratorPrivateKey, provider);  //Only instantiate if arbitrator is being used


// Create contract instances
const buyerContract = new ethers.Contract(contractAddress, contractABI, buyerWallet);
const sellerContract = new ethers.Contract(contractAddress, contractABI, sellerWallet);
const arbitratorContract = new ethers.Contract(contractAddress, contractABI, arbitratorWallet); //Only instantiate if arbitrator is being used



async function main() {
    try {
        // 1. Get the escrow amount from the contract (read-only call)
        const escrowAmount = await buyerContract.amount(); // Read the escrow amount
        console.log(`Escrow Amount: ${ethers.utils.formatEther(escrowAmount)} ETH`);


        // 2. Buyer deposits funds
        console.log("Buyer depositing funds...");
        const depositTx = await buyerContract.depositFunds({ value: escrowAmount });
        await depositTx.wait(); // Wait for the transaction to be mined
        console.log("Funds deposited successfully!");

        // 3. Seller releases funds
        console.log("Seller releasing funds...");
        const releaseTx = await sellerContract.releaseFunds();
        await releaseTx.wait();
        console.log("Funds released successfully!");

        // **** Alternative scenario: Dispute resolution ****
        /*
        // 3. Buyer opens a dispute
        console.log("Buyer opening a dispute...");
        const disputeTx = await buyerContract.openDispute("Item not as described");
        await disputeTx.wait();
        console.log("Dispute opened successfully!");


        // 4. Arbitrator resolves the dispute (example: send half to buyer, half to seller)
        console.log("Arbitrator resolving the dispute...");
        const halfAmount = escrowAmount.div(2); // Divide the escrow amount by 2
        const resolutionTx = await arbitratorContract.resolveDispute(
            buyerWallet.address, //Recipient: Buyer
            halfAmount, //Amount to send to buyer
            "Splitting funds equally due to item discrepancy." //Reason
        );
        await resolutionTx.wait();
        console.log("Dispute resolved successfully!");

        */


    } catch (error) {
        console.error("An error occurred:", error);
    }
}

main();
```

**Explanation:**

**Solidity (`Escrow.sol`):**

1.  **`pragma solidity ^0.8.0;`**: Specifies the Solidity compiler version.

2.  **`contract Escrow { ... }`**: Defines the smart contract named `Escrow`.

3.  **State Variables:**
    *   `seller` (address payable):  The Ethereum address of the seller.  Marked as `payable` to receive funds.
    *   `buyer` (address payable): The Ethereum address of the buyer. Marked as `payable` to receive funds.
    *   `arbitrator` (address): The Ethereum address of the arbitrator (optional). If set to a non-zero address, the arbitrator can resolve disputes.
    *   `amount` (uint): The amount of Ether (in Wei) that the buyer needs to deposit.
    *   `fundsDeposited` (bool): A flag indicating whether the buyer has deposited the required funds.
    *   `fundsReleased` (bool): A flag indicating whether the funds have been released to the buyer.
    *   `disputeOpened` (bool): A flag to indicate if a dispute has been opened.

4.  **Events:**  Events are used to log actions on the blockchain and can be listened to by external applications (like your JavaScript code).
    *   `FundsDeposited`: Emitted when the buyer deposits funds.
    *   `FundsReleased`: Emitted when the seller releases funds to the buyer.
    *   `DisputeOpened`: Emitted when the buyer opens a dispute.
    *   `DisputeResolved`: Emitted when the arbitrator resolves the dispute.
    *   `EscrowClosed`: Emitted when the escrow process is completed (either funds released or dispute resolved).

5.  **Constructor:**
    *   `constructor(address payable _seller, address payable _buyer, address _arbitrator, uint _amount) { ... }` : This function is executed only once when the smart contract is deployed.  It initializes the contract's state variables with the addresses of the seller, buyer, arbitrator (optional), and the amount to be held in escrow.
    *   `require()` statements ensure that the input values are valid (e.g., seller/buyer address is not the zero address, amount is greater than 0).

6.  **Modifiers:**  Modifiers are used to enforce conditions before executing a function.  This makes the code cleaner and more readable.
    *   `onlyBuyer`: Ensures that only the buyer can call the function.
    *   `onlySeller`: Ensures that only the seller can call the function.
    *   `onlyArbitrator`: Ensures that only the arbitrator can call the function.
    *   `fundsMustBeDeposited`: Ensures that the funds have been deposited before the function can be executed.
    *   `noDisputeOpened`:  Ensures that no dispute is currently open before the function can be executed.
    *   `disputeMustBeOpened`: Ensures that a dispute is currently open.

7.  **Functions:**
    *   `depositFunds()`:  Allows the buyer to deposit the required amount into the escrow.  It checks that the correct amount is sent with the transaction (`msg.value`).  It also prevents double-deposits.
    *   `releaseFunds()`: Allows the seller to release the funds to the buyer.  It checks that the funds have been deposited and that they haven't already been released. Uses `buyer.call{value: amount}("")` to transfer the funds.
    *   `openDispute(string calldata _reason)`:  Allows the buyer to open a dispute, providing a reason.
    *   `resolveDispute(address payable _recipient, uint _amount, string calldata _resolution)`:  Allows the arbitrator to resolve the dispute.  It takes the recipient (buyer or seller), the amount to be sent, and a resolution message as input.  Crucially, it transfers funds to the recipient using `_recipient.call{value: _amount}("")` and handles any remaining funds.  The function ensures that the arbitrator can only send funds up to the escrow amount and that the recipient is either the buyer or the seller.  It also sends remaining funds to the other party.
    *   `cancelEscrow()`: Allows either the buyer or seller to cancel the escrow, but *only* if funds haven't been deposited.  It uses `selfdestruct(payable(msg.sender))` to send any remaining gas back to the caller (buyer or seller).  **Important:**  `selfdestruct` is generally discouraged in production environments due to potential security risks. Consider alternative refund mechanisms.

**JavaScript (`script.js` or similar):**

1.  **Dependencies:**  Requires the `ethers.js` library.  Install it with `npm install ethers`.

2.  **Configuration:**
    *   `contractAddress`:  Replace with the address of your deployed `Escrow` contract. You get this after deploying the contract to a network (like Ganache).
    *   `contractABI`:  Replace with the ABI (Application Binary Interface) of your `Escrow` contract. The ABI describes the functions and events in your contract.  You can get this from Remix or other Solidity IDEs.  It's a JSON array.
    *   `providerURL`: The URL of your Ethereum provider (e.g., Ganache).  The default Ganache URL is `http://127.0.0.1:7545`.
    *   `buyerPrivateKey`, `sellerPrivateKey`, `arbitratorPrivateKey`:  Replace with the private keys of the buyer, seller, and arbitrator accounts.  **Never commit private keys to your repository!**  Use environment variables or secure configuration management in a real application.  Ganache provides a set of default private keys that you can use for testing.

3.  **Provider and Wallets:**
    *   Creates a `provider` using `ethers.providers.JsonRpcProvider(providerURL)`.
    *   Creates `buyerWallet`, `sellerWallet`, and `arbitratorWallet` using `new ethers.Wallet(privateKey, provider)`.  The wallets represent the Ethereum accounts that will interact with the contract.  Associate each wallet with the `provider`.

4.  **Contract Instances:**
    *   Creates instances of the `Escrow` contract using `new ethers.Contract(contractAddress, contractABI, signer)`.  The `signer` is the wallet that will be used to sign transactions.  Create separate contract instances for the buyer, seller, and arbitrator, using their respective wallets.

5.  **`main()` function:**
    *   This `async` function demonstrates how to interact with the contract.
    *   **Get Escrow Amount:** Calls the `amount()` function to read the required escrow amount.  Uses `ethers.utils.formatEther()` to format the Wei value into Ether for readability.
    *   **Buyer Deposits Funds:**
        *   Calls the `depositFunds()` function, sending the required `escrowAmount` as the `value` (Ether).  This is how Ether is sent along with the transaction.
        *   `await depositTx.wait()`:  Waits for the transaction to be mined (included in a block) before continuing. This is important because transactions are asynchronous.
    *   **Seller Releases Funds:**
        *   Calls the `releaseFunds()` function.
        *   `await releaseTx.wait()`: Waits for the transaction to be mined.
    *   **Alternative Dispute Resolution (Commented Out):**  This section shows how the buyer could open a dispute, and how the arbitrator could resolve it by sending funds to the buyer and/or seller.  This is commented out in the main example to focus on the basic escrow flow.
    *   **Error Handling:**  The `try...catch` block handles any errors that occur during the interaction with the contract.

**How to Run:**

1.  **Set up Ganache:**  Download and run Ganache.  Ganache provides a local Ethereum blockchain for development and testing.
2.  **Deploy the Contract:**  Deploy the `Escrow.sol` contract to Ganache using Remix or Truffle.  Note the contract address.
3.  **Update Configuration:**  Update the `contractAddress`, `contractABI`, `buyerPrivateKey`, `sellerPrivateKey`, and `arbitratorPrivateKey` variables in the JavaScript file with the correct values.  Use the private keys provided by Ganache for the buyer, seller, and arbitrator accounts.
4.  **Install Dependencies:**  Run `npm install ethers` in your project directory.
5.  **Run the Script:**  Run the JavaScript file using `node script.js` (or whatever you named the file).

**Important Considerations:**

*   **Security:**  This is a simplified example for educational purposes.  Real-world escrow contracts require careful security audits and best practices to prevent vulnerabilities.  Consider potential attacks like reentrancy attacks, denial-of-service attacks, and front-running.
*   **Error Handling:**  The error handling in this example is basic.  In a production system, you would need more robust error handling and logging.
*   **Gas Optimization:**  Smart contract code should be optimized to minimize gas costs.
*   **User Interface:**  In a real application, you would provide a user interface (UI) for users to interact with the escrow contract.
*   **Alternative Refund Mechanism:**  Instead of `selfdestruct` use a `withdraw()` function, only callable after a certain time or under specific conditions (e.g., buyer or seller agreement).  This is generally a safer approach.
*   **Upgradability:**  Smart contracts are generally immutable.  Consider using upgradeable contract patterns if you need to be able to update the contract logic in the future.  However, upgradability introduces additional complexity and risks.
*   **Arbitrator Selection:** In a real-world scenario, the method of selecting the arbitrator would need to be clearly defined (e.g. a third party service).
*   **Time Constraints:** Add time limits for actions (e.g., deposit, release, dispute).  After a certain time, allow a refund or automatic release of funds.

This comprehensive explanation and code example should help you understand the basics of creating an escrow service using Solidity and JavaScript. Remember to thoroughly test and audit your code before deploying it to a production environment.
👁️ Viewed: 8

Comments