-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from IoBuilders/fundable-implementation
Fundable implementation
- Loading branch information
Showing
20 changed files
with
9,944 additions
and
116 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'] | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
contracts/Migrations.sol |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.