Skip to content

Commit

Permalink
Add EIP-5528: Refundable Fungible Token (#5528)
Browse files Browse the repository at this point in the history
* initial draft for EIP-5503 Proposal

* update EPI name to 5528

* epi number fix continue

* fix EIP validator issue

* fix EIP validate issue

* applied change recommends

* Add EIP-20 to requires list

* structural update

* Make a few copyediting changes

* A few more copyediting changes

Co-authored-by: Pandapip1 <[email protected]>
  • Loading branch information
StartfundInc and Pandapip1 authored Sep 1, 2022
1 parent bab9d3f commit 60a9d44
Show file tree
Hide file tree
Showing 4 changed files with 499 additions and 0 deletions.
158 changes: 158 additions & 0 deletions EIPS/eip-5528.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
---
eip: 5528
title: Refundable Fungible Token
description: Allows refunds for EIP-20 tokens by escrow smart contract
author: StartfundInc (@StartfundInc)
discussions-to: https://ethereum-magicians.org/t/eip-5528-refundable-token-standard/10494
status: Draft
type: Standards Track
category: ERC
created: 2022-08-16
requires: 20
---

## Abstract

This standard is an extension of [EIP-20](./eip-20.md). This specification provides a type of escrow service in the blockchain ecosystem, which includes the following capabilities.

- The seller issues tokens.
- The seller creates an escrow smart contract with detailed escrow information. The information could include seller token contract address, buyer token contract address, lock period, exchange rate, the maximum number of buyers, minimum balance of buyers, additional escrow success conditions, etc.
- The seller funds seller tokens to the escrow contract.
- Buyers fund buyer tokens which are pre-defined in the escrow contract.
- When the escrow status meets success, the seller can withdraw buyer tokens, and buyers can withdraw seller tokens based on exchange rates.
- Buyers can withdraw (or refund) their funded token if the escrow process is failed or is in the middle of the escrow process.

## Motivation

Due to the nature of cryptocurrencies that guarantee anonymity, there is no way to get it back to the cryptocurrency that has already been paid.

To solve this problem, the Escrow service exists in the real world. However, it is challenging to implement an escrow service coordinated by a third-party arbitrator in a decentralized cryptocurrency ecosystem. To solve this, a smart contract was designed that acts as an escrow and devised a function where each token is sent back to the original wallet if the escrow is not completed.

Escrow smart contract service should support refund EIP-20 tokens in the middle of the escrow process or when the operation fails.

## Specification

There are two types of contract for the escrow process:

- `Payable Contract`: The sellers and buyers use this token to fund the `Escrow Contract`.
- `Escrow Contract`: Defines the escrow policies and holds `Payable Contract`'s token for a certain period.

This standard proposes interfaces on top of the [EIP-20](./eip-20.md) standard.

### Methods

#### constructor

The `Escrow Contract` may define the following policies:

- MUST include seller token contract address
- MUST include buyer token contract address
- Escrow period
- Maximum (or minimum) number of investors
- Maximum (or minimum) number of tokens to fund
- Exchange rates of seller/buyer token
- KYC verification of users

#### `escrowFund`

Funds `_value` amount of tokens to address `_to`.

In the case of `Escrow Contract`:

- `_to` MUST be the user address.
- `msg.sender` MUST be the payable contract address.
- MUST check policy validations.

In the case of `Payable Contract`:

- The address `_to` MUST be the escrow contract address.
- MUST call EIP-20's `_transfer` likely function.
- Before calling `_transfer` function, MUST call the same function of the escrow contract interface. The parameter `_to` MUST be `msg.sender` to recognize the user address in the escrow contract.

```solidity
function escrowFund(address _to, uint256 _value) public returns (bool)
```

#### `escrowRefund`

Refunds `_value` amount of tokens from address `_from`.

In the case of `Escrow Contract`:

- `_from` MUST be the user address.
- `msg.sender` MUST be the payable contract address.
- MUST check policy validations.

In the case of `Payable Contract`:

- The address `_from` MUST be the escrow contract address.
- MUST call EIP-20's `_transfer` likely function.
- Before calling `_transfer` function, MUST call the same function of the escrow contract interface. The parameter `_from` MUST be `msg.sender` to recognize the user address in the escrow contract.

```solidity
function escrowRefund(address _from, uint256 _value) public returns (bool)
```

#### `escrowWithdraw`

Withdraws funds from the escrow account.

In the case of `Escrow Contract`:
- MUST check the escrow process is completed.
- MUST send the remaining balance of seller and buyer tokens to `msg.sender`'s seller and buyer contract wallets.

In the case of `Payable Contract`, it is optional.

```solidity
function escrowWithdraw() public returns (bool)
```

### Example of interface

```solidity
pragma solidity ^0.4.20;
interface IERC5528 is ERC20 {
function escrowFund(address _to, uint256 _value) public returns (bool);
function escrowRefund(address to, uint256 amount) public returns (bool);
function escrowWithdraw() public returns (bool);
}
```

## Rationale

The interfaces described in this EIP have been chosen to cover the refundable issue in the escrow operation.

The suggested 3 functions (`escrowFund`, `escrowRefund` and `escrowWithdraw`) are based on `transfer` function in EIP-20.

`escrowFund` send tokens to the escrow contract. The escrow contract can hold the contract in the escrow process or reject tokens if the policy does not meet.

`escrowRefund` can be invoked in the middle of the escrow process or when the escrow process is failed.

`escrowWithdraw` allows users (sellers and buyers) to transfer tokens from the escrow account. When the escrow process is completed, the seller can get the buyer's token, and the buyers can get the seller's token.

## Backwards Compatibility

This EIP is fully backward compatible with the [EIP-20](./eip-20.md) specification.

## Test Cases

[Unit test example by truffle](../assets/eip-5528/truffule-test.js).

This test case demonstrates the following conditions for exchanging seller/buyer tokens.
- The exchange rate is one-to-one.
- If the number of buyers reaches 2, the escrow process will be terminated(success).
- Otherwise (not meeting success condition yet), buyers can refund (or withdraw) their funded tokens.

## Security Considerations

Since the escrow contract controls seller and buyer rights, flaws within the escrow contract will directly lead to unexpected behavior and potential loss of funds.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
55 changes: 55 additions & 0 deletions assets/eip-5528/ERC20Mockup.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
pragma solidity ^0.4.24;




contract ERC20Mockup {
mapping(address => uint256) _balances;
uint256 _totalSupply;
address _owner;

constructor(address initialAccount, uint256 initialBalance) {
_owner = initialAccount;
_totalSupply = initialBalance;
_balances[initialAccount] = initialBalance;
}

function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
function transfer(address to, uint256 amount) public returns (bool) {
address owner = msg.sender;
_transfer(owner, to, amount);
return true;
}

function _transfer(
address from,
address to,
uint256 amount
) internal {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");

uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
_balances[from] = fromBalance - amount;
_balances[to] += amount;
}
/*
From there, escrow related function
*/
function escrowFund(address to, uint256 amount) public returns (bool) {
bool res = ERC20Mockup(to).escrowFund(msg.sender, amount);
require(res, "Fund Failed");
_transfer(msg.sender, to, amount);

return true;
}
function escrowRefund(address to, uint256 amount) public returns (bool) {
bool res = ERC20Mockup(to).escrowRefund(msg.sender, amount);
require(res, "Refund Failed");
_transfer(to, msg.sender, amount);
return true;
}
}
171 changes: 171 additions & 0 deletions assets/eip-5528/EscrowContractAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
pragma solidity ^0.4.24;

import "./ERC20Mockup.sol";


contract ErcEscrowAccount {
struct BalanceData {
uint256 seller;
uint256 buyer;
}

enum State { Inited, Running, Success, Failed }

struct EscrowStatus {
uint256 numberOfBuyer;
uint256 fundTotal;
uint256 fundFilled;
State state;
}
mapping(address => BalanceData) _balances;

address _addrSeller;
address _addrBuyer;
address _addrEscrow;
address _addrCreator;

EscrowStatus _status;

constructor(uint256 fundAmount, address sellerContract, address buyerContract) {

//require(sellerContract.code.length > 0, "seller is not contract");
//require(buyerContract.code.length > 0, "buyer is not contract");

_addrBuyer = buyerContract;
_addrSeller = sellerContract;

_status.numberOfBuyer = 0;
_status.fundTotal = fundAmount;
_status.fundFilled = 0;

_addrEscrow = address(this);
_addrCreator = msg.sender;
_status.state = State.Inited;
}


function helper_bigInt256(uint256 _u256Val) public view returns (uint256) {
return _u256Val;
}

function helper_numberOfBuyers() public view returns (uint256) {
return _status.numberOfBuyer;
}

function _updateRunningState() {
if(_status.state == State.Running){
if(_status.numberOfBuyer == 2){
_status.state = State.Success;
}
}
}

function escrowStatus() public view returns (string) {
if(_status.state == State.Inited){
return "init";
}else if(_status.state == State.Running){
return "Running";
}else if(_status.state == State.Success){
return "Success";
}else if(_status.state == State.Failed){
return "Failed";
}
return "unknown state";
}


function balanceOf(address account) public view returns (uint256) {
return _balances[account].buyer;
}

function escrowBalanceOf(address account) public view returns (uint256 o_buyer, uint256 o_seller) {
o_buyer = _balances[account].buyer;
o_seller = _balances[account].seller;
}

function escrowFund(address to, uint256 amount) public returns (bool) {
require(amount > 0, "amount is too small");
if(msg.sender == _addrSeller){

require(_status.state == State.Inited, "must be init state");
require(to == _addrCreator, "to is only with creator");
require(amount == _status.fundTotal, "amount must be total fund");
require(_status.fundFilled == 0, "fund filled must be zero");

_status.fundFilled = amount;

_balances[to].seller = _balances[to].seller + amount;
_balances[to].buyer = 0;
_status.state = State.Running;

}else if(msg.sender == _addrBuyer){
require(_status.state == State.Running, "must be running state");
require(_status.fundTotal > 0, "escrow might be not started or already finished");
require(_status.fundFilled == _status.fundTotal, "fund does not filled yet");

// TODO: this logic is only for 1:1 exchange rate
require(amount <= _balances[_addrCreator].seller, "no more token left to exchange");

_balances[_addrCreator].seller = _balances[_addrCreator].seller - amount;
_balances[_addrCreator].buyer = _balances[_addrCreator].buyer + amount;

if(_balances[to].seller == 0){
_status.numberOfBuyer = _status.numberOfBuyer + 1;
}
_balances[to].seller = _balances[to].seller + amount;
_balances[to].buyer = _balances[to].buyer + amount;

_updateRunningState();
}else{
require(false, "Todo other cases");
}



return true;
}

function escrowRefund(address to, uint256 amount) public returns (bool) {
require(amount > 0, "amount is too small");
require(_status.state == State.Running || _status.state == State.Failed, "must be running state to refund");
require(msg.sender == _addrBuyer, "must be buyer contract to refund");
require(_balances[to].buyer >= amount, "buyer fund is not enough to refund");


_balances[to].buyer = _balances[to].buyer - amount;
_balances[to].seller = _balances[to].seller - amount;

_balances[_addrCreator].seller = _balances[_addrCreator].seller + amount;
_balances[_addrCreator].buyer = _balances[_addrCreator].buyer - amount;

if(_balances[to].buyer == 0){
_status.numberOfBuyer = _status.numberOfBuyer - 1;
}

_updateRunningState();
return true;
}

function escrowWithdraw() public returns (bool) {
address from = msg.sender;

if(from == _addrCreator){
if(_status.state == State.Success){
ERC20Mockup(_addrBuyer).transfer(from, _balances[from].buyer);
ERC20Mockup(_addrSeller).transfer(from, _balances[from].seller);

}else if(_status.state == State.Failed){
ERC20Mockup(_addrSeller).transfer(from, _status.fundFilled);
}else{
require(false, "invalid state for seller withdraw");
}
}else{
require(_status.state == State.Success, "withdraw is only in success, otherwise use refund");
ERC20Mockup(_addrSeller).transfer(from, _balances[from].seller);
}

delete _balances[from];
return true;
}

}
Loading

0 comments on commit 60a9d44

Please sign in to comment.