forked from DecentralizedClimateFoundation/DCIPs
267 lines
9.4 KiB
Markdown
267 lines
9.4 KiB
Markdown
|
---
|
||
|
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: Final
|
||
|
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 defines a type of escrow service with the following flow:
|
||
|
|
||
|
- The seller issues tokens.
|
||
|
- The seller creates an escrow smart contract with detailed escrow information like contract addresses, lock period, exchange rate, 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
|
||
|
|
||
|
Because of the pseudonymous nature of cryptocurrencies, there is no automatic recourse to recover funds that have already been paid.
|
||
|
|
||
|
In traditional finance, trusted escrow services solve this problem. In the world of decentralized cryptocurrency, however, it is possible to implement an escrow service without a third-party arbitrator. This standard defines an interface for smart contracts to act as an escrow service with a function where tokens are sent back to the original wallet if the escrow is not completed.
|
||
|
|
||
|
## 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*. This contract MUST override [EIP-20](./eip-20.md) interfaces.
|
||
|
- *Escrow Contract*: Defines the escrow policies and holds *Payable Contract*'s token for a certain period. This contract does not requires override [EIP-20](./eip-20.md) interfaces.
|
||
|
|
||
|
### Methods
|
||
|
|
||
|
#### `constructor`
|
||
|
|
||
|
The *Escrow Contract* demonstrates details of escrow policies as none-mutable matter in constructor implementation.
|
||
|
|
||
|
The *Escrow Contract* MUST define the following policies:
|
||
|
|
||
|
- Seller token contract address
|
||
|
- Buyer token contract address
|
||
|
|
||
|
The *Escrow Contract* MAY define the following policies:
|
||
|
|
||
|
- 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 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 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
|
||
|
|
||
|
This example demonstrates simple exchange of one seller and one buyer in one-to-one exchange rates.
|
||
|
|
||
|
```solidity
|
||
|
pragma solidity ^0.4.20;
|
||
|
|
||
|
interface IERC5528 {
|
||
|
|
||
|
function escrowFund(address _to, uint256 _value) public returns (bool);
|
||
|
|
||
|
function escrowRefund(address _from, uint256 _value) public returns (bool);
|
||
|
|
||
|
function escrowWithdraw() public returns (bool);
|
||
|
|
||
|
}
|
||
|
|
||
|
contract PayableContract is IERC5528, IERC20 {
|
||
|
/*
|
||
|
General ERC20 implementations
|
||
|
*/
|
||
|
|
||
|
function _transfer(address from, address to, uint256 amount) internal {
|
||
|
uint256 fromBalance = _balances[from];
|
||
|
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
|
||
|
_balances[from] = fromBalance - amount;
|
||
|
_balances[to] += amount;
|
||
|
}
|
||
|
|
||
|
function transfer(address to, uint256 amount) public returns (bool) {
|
||
|
address owner = msg.sender;
|
||
|
_transfer(owner, to, amount);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
function escrowFund(address _to, uint256 _value) public returns (bool){
|
||
|
bool res = IERC5528(to).escrowFund(msg.sender, amount);
|
||
|
require(res, "Fund Failed");
|
||
|
_transfer(msg.sender, to, amount);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
function escrowRefund(address _from, uint256 _value) public returns (bool){
|
||
|
bool res = IERC5528(_from).escrowRefund(msg.sender, _value);
|
||
|
require(res, "Refund Failed");
|
||
|
_transfer(_from, msg.sender, _value);
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
contract EscrowContract is IERC5528 {
|
||
|
|
||
|
enum State { Inited, Running, Success, Closed }
|
||
|
struct BalanceData {
|
||
|
address addr;
|
||
|
uint256 amount;
|
||
|
}
|
||
|
|
||
|
address _addrSeller;
|
||
|
address _addrBuyer;
|
||
|
BalanceData _fundSeller;
|
||
|
BalanceData _fundBuyer;
|
||
|
EscrowStatus _status;
|
||
|
|
||
|
constructor(address sellerContract, address buyerContract){
|
||
|
_addrSeller = sellerContract;
|
||
|
_addrBuyer = buyerContract;
|
||
|
_status = State.Inited;
|
||
|
}
|
||
|
|
||
|
function escrowFund(address _to, uint256 _value) public returns (bool){
|
||
|
if(msg.sender == _addrSeller){
|
||
|
require(_status.state == State.Running, "must be running state");
|
||
|
_fundSeller.addr = _to;
|
||
|
_fundSeller.amount = _value;
|
||
|
_status = State.Success;
|
||
|
}else if(msg.sender == _addrBuyer){
|
||
|
require(_status.state == State.Inited, "must be init state");
|
||
|
_fundBuyer.addr = _to;
|
||
|
_fundBuyer.amount = _value;
|
||
|
_status = State.Running;
|
||
|
}else{
|
||
|
require(false, "Invalid to address");
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
function escrowRefund(address _from, uint256 amount) public returns (bool){
|
||
|
require(_status.state == State.Running, "refund is only available on running state");
|
||
|
require(msg.sender == _addrBuyer, "invalid caller for refund");
|
||
|
require(_fundBuyer.addr == _from, "only buyer can refund");
|
||
|
require(_fundBuyer.amount >= amount, "buyer fund is not enough to refund");
|
||
|
_fundBuyer.amount = _fundBuyer.amount - amount
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
function escrowWithdraw() public returns (bool){
|
||
|
require(_status.state == State.Success, "withdraw is only available on success state");
|
||
|
uint256 common = MIN(_fundBuyer.amount, _fundSeller.amount);
|
||
|
|
||
|
if(common > 0){
|
||
|
_fundBuyer.amount = _fundBuyer.amount - common;
|
||
|
_fundSeller.amount = _fundSeller.amount - common;
|
||
|
|
||
|
// Exchange
|
||
|
IERC5528(_addrSeller).transfer(_fundBuyer.addr, common);
|
||
|
IERC5528(_addrBuyer).transfer(_fundSeller.addr, common);
|
||
|
|
||
|
// send back the remaining balances
|
||
|
if(_fundBuyer.amount > 0){
|
||
|
IERC5528(_addrBuyer).transfer(_fundBuyer.addr, _fundBuyer.amount);
|
||
|
}
|
||
|
if(_fundSeller.amount > 0){
|
||
|
IERC5528(_addrSeller).transfer(_fundSeller.addr, _fundSeller.amount);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_status = State.Closed;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
```
|
||
|
|
||
|
## Rationale
|
||
|
|
||
|
The interfaces cover the escrow operation's refundable issue.
|
||
|
|
||
|
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 fails.
|
||
|
|
||
|
`escrowWithdraw` allows users (sellers and buyers) to transfer tokens from the escrow account. When the escrow process completes, the seller can get the buyer's token, and the buyers can get the seller's token.
|
||
|
|
||
|
## Backwards Compatibility
|
||
|
|
||
|
The *Payable Contract* which implements 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).
|