Try   HackMD

Smart contracts: Liquidity for bone marrow donor registries

(This document was started on 4 Mar 2019)

Motivation

Main bottleneck: Registries have to invest significant resources and time in raising money to recruit donors. This increases barrier to entry for small registries in underdeveloped areas. Lack of liquidity also tends to decrease efficiency of daily operations.

Solution: Allow lenders to provide funding for donor recruitment, in exchange for a "license fee" on each successful donor-patient match.

Method: Smart contracts with funding and licensing capabilities, programmatically disbursing license fees to lenders when a donor-patient match is made.

Design

Funding request and fulfilment

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  1. Registry initiates fundingRequest by escrowing licenseFee for n donors into License Contract, with the promise of registering them before a certain _timeout. Before a lender funds their request, Registry can withdraw these escrowed fees.
  2. Lender fulfils the funding request by depositing recruitFund to License Contract, addressed to Registry on the condition that n donors are recruited before _timeout.
  3. License Contract sends recruitFund to Registry, on the condition that n donors are registered before _timeout. If this condition is not met, the escrowed licenseFee is sent to Lender.
  4. Registry registers n donors (before _timeout) on License Contract, assigning the license rights for these donors to the Lender.
  5. When a donor-patient match is made, the Patient pays a matchingFee to the License Contract.
  6. License Contract disburses matchingFee + licenseFee to Lender.

Donor registration and approval

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  1. Registry sends donorHash to License Contract. This is a hash of a JSON file (or other format) containing donors' information, and stored off-chain.
  2. Registry sends the raw JSON file to Lender off-chain, who verifies that it meets certain pre-agreed criteria (e.g. age, gender, race) and hashes to the on-chain donorHash.
  3. Lender approves donorHash on LicenseContract.
  4. For a certain approved donorHash, when a donor-patient match is made, the Patient pays a matchingFee to the License Contract.
  5. License Contract disburses matchingFee + licenseFee to Lender.

Specification

(Solidity pseudocode)

Contract state

The License Contract should keep track of whitelisted registries and lenders, as well as funding requests and registered donors.

contract LicenseContract(){ //mapping from whitelisted registries' Ethereum //addresses to registry names mapping(address=>string) public registryAddresses; //mapping from whitelisted registries' Ethereum //addresses to amount escrowed by them mapping(address=>string) public registryEscrows; //mapping from whitelisted lenders' Ethereum //addresses to lender names mapping(address=>string) public lenderAddresses; //mapping from indices to funding requests mapping (uint=>FundingRequest) public fundingRequests; ![Uploading file..._0anx2horv]() //number of funding requests uint public numRequests; //mapping from hashed information of registered donors to //registries who recruited them under a given funding request mapping(bytes=>(address=>uint)) public donorHashToRegistry; //mapping from hashed information of registered donors to //patients who matched with them mapping(bytes=>address) public donorHashToPatient; //donor registration event, initiated by registry event RegisterDonor(bytes donorHash, uint requestIdx); //donor approval event, done by lender event ApproveDonor(bytes donorHash, uint requestIdx); //... }

(Which authority/authorities will whitelist registries and lenders?)

//TODO: set whitelisting permissions function whitelistRegistry(address _registryAddr, string _registryName){ registryAddresses[_registryAddr] = _registryName; } //TODO: set whitelisting permissions function whitelistLender(address _lenderAddr, string _lenderName){ lenderAddresses[_lenderAddr] = _lenderName; }

Contract methods

Requests for funding

Each request for funding should specify the Registry from which it originates, the no. of donors it promises to recruit, the license fee paid out per successful donor match, and the timeout within which these donors must be registered.

//requests made by registries for funding struct FundingRequest{ address registry; //registry making the funding request uint promisedDonors; //no. of donors promised uint licenseFee; //license fees promised per donor uint amt; //funding amount requested uint timeout; //timeout for promised recruitment in seconds uint startTime; //time when request was fulfilled uint numDonors; //no. of donors recruited string status; //status of funding request address lender; //lender fulfilling the funding request }

A registry must escrow its promised licensing fees (which are far less than the funds borrowed from lenders) to create a funding request. This provides some degree of collateral for the request: the escrowed fees are sent to the lender in case of failure to recruit the promised number of donors.

//registries escrow licenseFee to create FundingRequest function requestFunding( address _registry, uint _promisedDonors, uint _licenseFee, uint _amt, uint _timeout ) public payable { require(msg.value >= _numDonors * _licenseFee, "registry stake must at least equal license fees"); require(registryAddresses[msg.sender] != address(0), "registry must be whitelisted"); //create FundingRequest object FundingRequest _fundingRequest = FundingRequest( _registry, _promisedDonors, _licenseFee, _amt, _timeout, 0, //timer has not started yet 0, //no donors recruited yet "pending", //request has pending status address(0) //no lender has yet fulfilled request ); //add request to fundingRequests mapping fundingRequests[numRequests] = _fundingRequest; //increase counter for no. or requests made numRequests++; //record amount escrowed by registry registryEscrows[msg.sender] = msg.value; }

Fulfilling requests for funding

//lenders fulfil a funding request function fulfilFunding(uint _requestIdx) public payable{ require(registryAddresses[msg.sender] != address(0), "lender must be whitelisted"); FundingRequest request = fundingRequests[_requestIdx] require(msg.value >= request.amt, "funds must at least equal requested funds"); //send funds to registry who made the request request.registry.send(msg.value); //start timer request.startTime = block.timestamp; //mark request as fulfilled request.status = "fulfilled"; //set lender address request.lender = msg.sender; }

Registering donors

Most of donors' information should be stored offchain; a hash of it (maybe IPFS hash?) can be committed on-chain by the registry.

function registerDonor(bytes _donorHash, uint _requestIdx){ FundingRequest request = fundingRequests[_requestIdx]; require(registryAddresses[msg.sender] == request.registry, "registry did not make this request"); emit RegisterDonor(_donorHash, _requestIdx) }

Lenders should be able to vet the quality of donors and only acknowledge them if they meet certain pre-agreed criteria (e.g. age, gender, race, etc.); this verification is done off-chain.

function approveDonor(bytes _donorHash, uint _requestIdx){ FundingRequest request = fundingRequests[_requestIdx]; require(registryAddresses[msg.sender] == request.lender, "lender did not fund this request"); //add donor to list recruited by registry, for given funding request donorHashToRegistry[_donorHash][msg.sender] = _requestIdx; //add to donor count under given funding request fundingRequests[_requestIdx].numDonors++; emit ApproveDonor(_donorHash, _requestIdx); }

Lender payout

When a match is made, the patient (or any party on behalf of the patient) should pay a matching fee to the smart contract. This matching fee is then transferred to the lender who funded the donor recruitment, along with the promised license fee.

function payForMatch(bytes _donorHash) public payable{ require(donorHashToPatient[_donorHash] != address(0), "donor already has a match"); //registry which recruited donor address registry = donorHashToPatient[_donorHash]; //amt left in registry escrow uint escrow = registryEscrows[registry] //funding request under which donor was recruited uint requestIdx = donorHashToPatient[_donorHash][registry]; FundingRequest request = fundingRequests[_requestIdx]; require(escrow >= request.licenseFee, "registry does not have enough escrowed to cover license fee"); //lender which funded request address lender = request.lender; //send matching fee and license fee to lender lender.send(msg.value); lender.send(request.licenseFee); //deduct license fee from registry escrow registryEscrows[registry] = escrow - request.licenseFee; }

Slashing escrow

If funded registries do not fulfil their recruitment promises in time, the lender can initiate slashEscrow to collect the registry's escrowed license fees. (Of course, the lender still loses the funds they lent; any possible better designs?)

function slashEscrow(uint _requestIdx) public { FundingRequest request = fundingRequests[_requestIdx]; require(registryAddresses[msg.sender] == request.lender, "lender did not fund this request"); if (request.startTime > block.timestamp - request.timeout && request.numDonors < request.promisedDonors){ //send escrowed license fees to lender msg.sender.send(registryEscrows[registry]); //deduct license fee from registry escrow registryEscrows[registry] = 0; } }