Skip to content

Commit

Permalink
Merge pull request #1 from IoBuilders/fundable-implementation
Browse files Browse the repository at this point in the history
Fundable implementation
  • Loading branch information
Ferparishuertas committed Jul 22, 2019
2 parents 2d8d37b + 6046fb9 commit a898f6c
Show file tree
Hide file tree
Showing 20 changed files with 9,944 additions and 116 deletions.
4 changes: 4 additions & 0 deletions .solcover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
testrpcOptions: '-p 8555 -e 1000000 -g 0x01',
copyPackages: ['openzeppelin-solidity']
};
2 changes: 2 additions & 0 deletions .soliumignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
contracts/Migrations.sol
9 changes: 9 additions & 0 deletions .soliumrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "solium:all",
"plugins": ["security"],
"rules": {
"quotes": ["error", "double"],
"indentation": ["error", 4],
"linebreak-style": ["error", "unix"]
}
}
13 changes: 13 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
sudo: required
dist: xenial
language: node_js
node_js:
- '10'
install:
- npm install -g coveralls
- npm install
script:
- npm run lint
- npm test
after_script:
- npm run coverage && cat coverage/lcov.info | coveralls
95 changes: 94 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,98 @@
# Fundable Token

This is the work in progress implementation of [EIP-2019 Fundable token](https://github.com/ethereum/EIPs/pull/2019/files). This implementation will change over time with the standard and is not stable at the moment.
[![Build Status](https://travis-ci.org/IoBuilders/fundable-token.svg?branch=master)](https://travis-ci.org/IoBuilders/fundable-token)
[![Coverage Status](https://coveralls.io/repos/github/IoBuilders/fundable-token/badge.svg?branch=master)](https://coveralls.io/github/IoBuilders/fundable-token?branch=master)
[![npm](https://img.shields.io/npm/v/eip2019.svg)](https://www.npmjs.com/package/eip2019)

This is the reference implementation of [EIP-2019 Fundable token](https://github.com/ethereum/EIPs/pull/2019/files). This implementation will change over time with the standard and is not stable at the moment.

Feedback is appreciated and can given at [the discussion of the EIP](https://github.com/ethereum/EIPs/issues/2019).

## Summary

An extension to the ERC-20 standard token that allows Token wallet owners to request a wallet to be funded, by calling the smart contract and attaching a fund instruction string.

## Abstract

Token wallet owners (or approved addresses) can order tokenization requests through blockchain. This is done by calling the ```orderFund``` or ```orderFundFrom``` methods, which initiate the workflow for the token contract operator to either honor or reject the fund request. In this case, fund instructions are provided when submitting the request, which are used by the operator to determine the source of the funds to be debited in order to do fund the token wallet (through minting).

In general, it is not advisable to place explicit routing instructions for debiting funds on a verbatim basis on the blockchain, and it is advised to use a private communication alternatives, such as private channels, encrypted storage or similar, to do so (external to the blockchain ledger). Another (less desirable) possibility is to place these instructions on the instructions field in encrypted form.

## Sequence diagrams

### Fund executed

The following diagram shows the sequence of the payout creation and execution.

![Fundable Token: Fund executed](http://www.plantuml.com/plantuml/png/ZP0n3i8m34NtdCBgtWime7O0YGa6E41eVXae3h8JYUFJr7H11NNCz_FFanjDNb9-3EwYa9RgBLNxpC5V1z0vti7LXg84I4dTzupgUO7Q6pYDS7aSom8CdoVBrK-97LJ_b4zUdzu3dunt5bC_XhfaaSIpzX0ZLeZWXIud_1QPFZIDdR71DU1GRlS6)

### Fund cancelled

The following diagram shows the sequence of the payout creation and cancellation.

![Fundable Token: Fund cancelled](http://www.plantuml.com/plantuml/png/SoWkIImgAStDuGejJYroLD2rKr1oAyrBIKpAILK8oSzEpLEoKiWlIaaj0eboeSifwC8qA3Ycf-QL01M3EFuW3Qaf-CnCJinBJiqXnL1di8uSeB4EgNaf82S30000)

### Fund rejected

The following diagram shows the sequence of the payout creation and rejection.

![Fundable Token: Fund rejected](http://www.plantuml.com/plantuml/png/SoWkIImgAStDuGejJYroLD2rKr1oAyrBIKpAILK8oSzEpLEoKiWlIaaj0eboeSifwC8qA3Ycf-QL01M3EFuW3QaWvGWPx4ONfMQb9fVWCHliBAYnGM35G7CTKlDIG6u60000)

## State diagram

![Fundable: State Diagram](http://www.plantuml.com/plantuml/png/VL51JiGm3Bpd5ND6x0Sue9KGI9n0g3V48KtSfP2rLuaZwEzfqsspArMSeh77C_PadzH6pSTWtcy-iDlTuoLwYkJly9JPdu4vluNmpAzH7AKqKrPerib6leaJR2ISY7tF1wYW7T7C98zsW4KtZiCUYDLSY3QVD7VaHD5gBumVcr0M9N-BDYjqv6XrOL4CfEYvT5eRB3k2T0NcHB4Qb1iUVybbNQvSaAdbvjhWsFDOHYUnAbvcyZ3vXR08hj3KniI1S1Yc89mDeQImBVT6N-JMzHPKR_YFLClRXbUnxudzzFb_)

## Install

```
npm install eip2019
```

## Usage

To write your custom contracts, import the contract and extend it through inheritance.

```solidity
pragma solidity ^0.5.0;
import 'eip2019/contracts/Fundable.sol';
contract MyFundable is Fundable {
// your custom code
}
```

> You need an ethereum development framework for the above import statements to work! Check out these guides for [Truffle], [Embark] or [Buidler].
## Fund information

Whenever a payout is ordered, payment information has to be provided with the necessary information for the off-chain transfer. [EIP-2019](https://github.com/ethereum/EIPs/pull/2019/files) leaves the structure of this information up to the implementer, but recommends [ISO-20022](https://en.wikipedia.org/wiki/ISO_20022) as a starting point.

The unit tests use a JSON version of this standard, which can be seem below.

```json
{
"messageId": "Example Message ID",
"funds": [
{
"amount": 1.00,
"fundingSubjectId": "caaa2bd3-dc42-436a-b70b-d1d7dac23741",
"receiverInformation": "Example funds receiver information"
}
]
}
```

Amongst other things, if defines the funded amount, an ID to a predefined bank account or credit card and the receiver information. Additionally some IDs are defined to properly mark the transfer.

## Tests

To run the unit tests execute `npm test`.

## Code coverage

To get the code coverage report execute `npm run coverage`

[Truffle]: https://truffleframework.com/docs/truffle/quickstart
[Embark]: https://embark.status.im/docs/quick_start.html
[Buidler]: https://buidler.dev/guides/#getting-started
32 changes: 32 additions & 0 deletions contracts/FundAgentRole.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
pragma solidity ^0.5.0;

import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "openzeppelin-solidity/contracts/access/Roles.sol";


contract FundAgentRole is Ownable {
using Roles for Roles.Role;

Roles.Role internal fundAgents;

modifier onlyFundAgent() {
_onlyFundAgent();
_;
}

function addFundAgent(address _who) public onlyOwner {
fundAgents.add(_who);
}

function removeFundAgent(address _who) public onlyOwner {
fundAgents.remove(_who);
}

function isFundAgent(address _who) public view returns (bool) {
return fundAgents.has(_who);
}

function _onlyFundAgent() private view {
require(isFundAgent(msg.sender), "FundAgentRole: caller is not a fund agent");
}
}
173 changes: 173 additions & 0 deletions contracts/Fundable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
pragma solidity ^0.5.0;

import "./IFundable.sol";
import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import "./libraries/StringUtil.sol";
import "./FundAgentRole.sol";


contract Fundable is IFundable, ERC20, FundAgentRole {
using StringUtil for string;

struct FundableData {
address orderer;
address walletToFund;
uint256 value;
string instructions;
FundStatusCode status;
}

// walletToFund -> authorized -> true/false
mapping(address => mapping(address => bool)) public fundOperators;
mapping(bytes32 => FundableData) private orderedFunds;

constructor() public {
fundAgents.add(msg.sender);
}

function authorizeFundOperator(address orderer) public returns (bool) {
require(fundOperators[msg.sender][orderer] == false, "The operator is already authorized");

fundOperators[msg.sender][orderer] = true;
emit FundOperatorAuthorized(msg.sender, orderer);
return true;
}

function revokeFundOperator(address orderer) public returns (bool) {
require(fundOperators[msg.sender][orderer], "The operator is already not authorized");

fundOperators[msg.sender][orderer] = false;
emit FundOperatorRevoked(msg.sender, orderer);
return true;
}

function orderFund(
string memory operationId,
uint256 value,
string memory instructions
) public returns (bool)
{
return _orderFund(
operationId,
msg.sender,
value,
instructions
);
}

function orderFundFrom(
string memory operationId,
address walletToFund,
uint256 value,
string memory instructions
) public returns (bool)
{
require(address(0) != walletToFund, "WalletToFund address must not be zero address");
require(_isFundOperatorFor(msg.sender, walletToFund), "This operator is not authorized");
return _orderFund(
operationId,
walletToFund,
value,
instructions
);
}

function cancelFund(string memory operationId) public returns (bool) {
FundableData storage fund = orderedFunds[operationId.toHash()];
require(fund.status == FundStatusCode.Ordered, "A fund can only be cancelled in status Ordered");
require(fund.walletToFund == msg.sender || fund.orderer == msg.sender, "Only the wallet who receives the fund can cancel");
fund.status = FundStatusCode.Cancelled;
emit FundCancelled(fund.orderer, operationId);
return true;
}

function processFund(string memory operationId) public onlyFundAgent returns (bool) {
FundableData storage fund = orderedFunds[operationId.toHash()];
require(fund.status == FundStatusCode.Ordered, "A fund can only be put in process from status Ordered");
fund.status = FundStatusCode.InProcess;
emit FundInProcess(fund.orderer, operationId);
return true;
}

function executeFund(string memory operationId) public onlyFundAgent returns (bool) {
FundableData storage fund = orderedFunds[operationId.toHash()];
require(fund.status == FundStatusCode.InProcess, "A fund can only be executed from status InProcess");
fund.status = FundStatusCode.Executed;
_mint(fund.walletToFund, fund.value);
emit FundExecuted(fund.orderer, operationId);
return true;
}

function rejectFund(
string memory operationId,
string memory reason
) public onlyFundAgent returns (bool)
{
FundableData storage fund = orderedFunds[operationId.toHash()];
require(
fund.status == FundStatusCode.Ordered || fund.status == FundStatusCode.InProcess,
"A fund can only be rejected if the status is ordered or in progress"
);

fund.status = FundStatusCode.Rejected;
emit FundRejected(fund.orderer, operationId, reason);
return true;
}

function isFundOperatorFor(address walletToFund, address orderer) public view returns (bool) {
return _isFundOperatorFor(walletToFund, orderer);
}

function retrieveFundData(
string memory operationId
) public view returns (
address orderer,
address walletToFund,
uint256 value,
string memory instructions,
FundStatusCode status
)
{
FundableData storage fund = orderedFunds[operationId.toHash()];
orderer = fund.orderer;
walletToFund = fund.walletToFund;
value = fund.value;
instructions = fund.instructions;
status = fund.status;
}

function _orderFund(
string memory operationId,
address walletToFund,
uint256 value,
string memory instructions
) private returns (bool)
{
require(!instructions.isEmpty(), "Instructions must not be empty");
require(!operationId.isEmpty(), "operationId must not be empty");
require(value > 0, "Value must be greater than zero");

FundableData storage newFund = orderedFunds[operationId.toHash()];
require(newFund.value == 0, "This operationId already exists");

newFund.orderer = msg.sender;
newFund.walletToFund = walletToFund;
newFund.value = value;
newFund.instructions = instructions;
newFund.status = FundStatusCode.Ordered;
orderedFunds[operationId.toHash()] = newFund;

emit FundOrdered(
msg.sender,
operationId,
walletToFund,
value,
instructions
);
return true;
}

function _isFundOperatorFor(address operator, address from) private view returns (bool) {
return fundOperators[from][operator];
}
}
48 changes: 48 additions & 0 deletions contracts/IFundable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
pragma solidity ^0.5.0;

interface IFundable {
enum FundStatusCode {
NonExistent,
Ordered,
InProcess,
Executed,
Rejected,
Cancelled
}
function authorizeFundOperator(address orderer) external returns (bool);
function revokeFundOperator(address orderer) external returns (bool);
function orderFund(string calldata operationId, uint256 value, string calldata instructions) external returns (bool);
function orderFundFrom(
string calldata operationId,
address walletToFund,
uint256 value,
string calldata instructions
) external returns (bool);
function cancelFund(string calldata operationId) external returns (bool);
function processFund(string calldata operationId) external returns (bool);
function executeFund(string calldata operationId) external returns (bool);
function rejectFund(string calldata operationId, string calldata reason) external returns (bool);
function isFundOperatorFor(address walletToFund, address orderer) external view returns (bool);
function retrieveFundData(
string calldata operationId
) external view returns (
address orderer,
address walletToFund,
uint256 value,
string memory instructions,
FundStatusCode status
);
event FundOrdered(
address indexed orderer,
string operationId,
address indexed walletToFund,
uint256 value,
string instructions
);
event FundInProcess(address indexed orderer, string operationId);
event FundExecuted(address indexed orderer, string operationId);
event FundRejected(address indexed orderer, string operationId, string reason);
event FundCancelled(address indexed orderer, string operationId);
event FundOperatorAuthorized(address indexed walletToFund, address indexed orderer);
event FundOperatorRevoked(address indexed walletToFund, address indexed orderer);
}

0 comments on commit a898f6c

Please sign in to comment.