User Story:
As a user, I want to deploy my own contracts in the side chain. To do this I need OST ' on the auxiliary chain.
Scenario:
As a company/user, I want to write contracts like custom rules. To get OST' on test net we already have faucet that can provide the OST'. But for mainnet, it is still a manual/centralized work. Instead of this, it will be helpful for the user if they can stake OST on origin chain and mint OST' on an auxiliary chain.
Proposal:
“Composer” contract, where (open or whitelisted) facilitator pool can accept (or ignore, and a user can revoke) stakeRequests of OST-> OST’ on mainnet; MVP contract + executable (like a faucet)
Specifications:
Composer contract:Please note: the contract is referred to Composer, but this is not final name. Suggest a better name.
Composer contract will be Organized
Single Composer contract must work for arbitrary gateway.
Any address(user) must be able to request stake for OST on origin chain by calling requestStake function.
Facilitators (whitelisted, for now, later it can be open) can facilitate the stake and mint process on behalf of the user by providing the hash lock. A whitelisted address can call acceptStakeRequest to facilitate stake and mint.
function acceptStakeRequest(
bytes32 _stakeRequestHash,
bytes32 _hashLock
)
onlyWorker // only whitelisted address can call this
external
returns (bytes32 messageHash_)
The staker can revoke the stake request any time unless it has been accepted and processed by the facilitator. Staker can call revokeStakeRequest() function to revoke the stake request. Staker can revoke only its own stake requests.
function revokeStakeRequest(
bytes32 _stakeRequestHash
)
external
A facilitator can reject the stakeRequest by calling rejectStakeRequest. We should evaluate if this function is needed. If the facilitator pool will be open then we may not need this function.
function rejectStakeRequest(
bytes32 _stakeRequestHash
)
onlyWorker // only whitelisted address can call this
external
All events of the composer contracts must be sufficient for the facilitator to decide if they want to accept or ignore the facilitation.
<To be added>
Currently, there is a limitation of one stake request at a time in the Gateway contracts. This must be gracefully handled in the composer contract. One of the proposals is given below that uses proxy contract for staker.
The organization can deactivate the new stake requests. By doing this no one can call stakeRequest. Facilitators can still accept stake requests. Staker can still revoke the stake requests.
StakerProxy contract:
Please note: the contract is referred to StakerProxy, but this is not final name. Suggest a better name.
Gateways can do one stake - mint process at a time for a given staker address. So the composer contract cannot call gateway.stake() function, it will be not able to serve all stake requests from multiple stakers addresses. So a proxy contract is introduced as a proposal here.
This contract has a minimal code, that gets deployed from the Composer contract.
It is deployed per user once.
It can be used for arbitrary gateways.
Actual staker is the owner, and it can be destroyed by the owner only.
All other functions can be called only by Composer contract.
Add workers (non interactive)
Since the workers will need to do transactions, the worker account must be unlocked.
For this we should do it in non-interactive way by providing the password path
Adding Single worker
./facilitator addworker <address> --non-interactive ./password.txt --chain-ids <chainId,...>
List worker balances in all chain ids
./facilitator listworkersbalance
List stakeRequests
./facilitator liststakerequests <chainid>
Handle SIGTERM gracefully
Implementation Proposal
Gateways can do one stake - mint process at a time. Also, the stake function of gateway contract is called by the staker address.
There are multiple ways to implement this
First is like existing gateway composer for branded token. It requires one gateway composer per user. This will require a bookkeeper that tracks all the composers (onchain in contract or off-chain in DB)
The second way is to have a single composer that internally manages the proxy staker account.
In this approach, staker can call stakeRequest on Composer contract. Composer contract internally deploys a proxy contract per user (only once per user).
Proxy contract is just used to maintain the nonce and acts as the staker. This proxy contract can be called only by the Composer contract.
An incomplete rough implementation is available here.
Composer.sol
pragma solidity ^0.5.0;
import "./GatewayInterface.sol";
import "./Organized.sol";
import "./StakerProxy.sol";
import "./EIP20Interface.sol";
contract Composer is Organized {
// Stake request struct.
struct StakeRequest {
uint256 amount;
address beneficiary;
uint256 gasPrice;
uint256 gasLimit;
uint256 nonce;
address staker;
address gateway;
}
// Maps staker address to its proxy contract.
mapping (address => StakerProxy) public stakerProxies;
// Maps staker address to stake request hash.
mapping (address => bytes32) public StakeRequestHashes;
// Maps stake request hash to StakeRequest.
mapping (bytes32 => StakeRequest) public stakeRequests;
// constructor
constructor(
OrganizationInterface _organization
)
public
Organized(_organization)
{
}
// - Staker can call this function.
// - Staker needs to approve Composer contract for stake amount.
function requestStake(
uint256 _amount,
address _beneficiary,
uint256 _gasPrice,
uint256 _gasLimit,
uint256 _nonce,
address _gateway
)
external
returns (
bytes32 stakeRequestHash_
)
{
// Do basic validations.
// get stakerProxy
StakerProxy stakerProxy = stakerProxies[msg.sender];
// if staker proxy contract addresses does not exists.
if (address(stakerProxy) == address(0)) {
// deploy the proxy contract.
stakerProxy = new StakerProxy(
address(this),
msg.sender
);
stakerProxies[msg.sender] = stakerProxy;
}
// generate stakeRequestHash_;
StakeRequest storage stakeRequest = stakeRequests[stakeRequestHash_];
require(stakeRequest.staker == address(0), 'Staker request is already in progress.');
uint256 nonce = GatewayInterface(_gateway).getNonce(msg.sender);
require(_nonce == nonce, "Nonce must match.");
// store stake requests in proxy contract.
// update the stakeRequests mapping.
stakeRequests[stakeRequestHash_] = StakeRequest(
_amount,
_beneficiary,
_gasPrice,
_gasLimit,
_nonce,
msg.sender,
_gateway
);
// Transfer the stake amount to this address.
address token = GatewayInterface(_gateway).token();
require(
EIP20Interface(token).transferFrom(msg.sender, address(this), _amount),
"Stake amount must be transferred to Composer."
);
// Emit stake requested event.
}
// This can be called by the workers (white listed facilitators).
// workers/facilitators needs to approve the Composer contract for bounty amount Transfer.
function acceptStakeRequest(
bytes32 _stakeRequestHash,
bytes32 _hashLock
)
onlyWorker
external
returns (bytes32 messageHash_)
{
// do basic validations.
StakeRequest storage stakeRequest = stakeRequests[_stakeRequestHash];
require(stakeRequest.staker != address(0), 'Staker request must exists.');
GatewayInterface gateway = GatewayInterface(stakeRequest.gateway);
delete stakeRequests[_stakeRequestHash];
// get stakerProxy
StakerProxy stakerProxy = stakerProxies[msg.sender];
require(address(stakerProxy) != address(0), 'Staker proxy must exists.');
// bount amount.
uint256 bounty = gateway.bounty();
// Transfer the bounty amount to stakerProxy.
address token = gateway.token();
require(
EIP20Interface(token).transferFrom(msg.sender, address(stakerProxy), bounty),
"bounty amount must be transferred to stakerProxy."
);
address baseToken = gateway.baseToken();
// transfer the stake amount
require(
EIP20Interface(baseToken).transfer(address(stakerProxy), stakeRequest.amount),
"Stake amount must be transferred to stakerProxy."
);
messageHash_ = stakerProxy.acceptStakeRequest(
stakeRequest.amount,
stakeRequest.beneficiary,
stakeRequest.gasPrice,
stakeRequest.gasLimit,
stakeRequest.nonce,
_hashLock,
stakeRequest.staker,
stakeRequest.gateway
);
// Emit event
}
// For below 2 functions the common code can be extracted.
function rejectStakeRequest(
bytes32 _stakeRequestHash
)
onlyWorker
external
{
// Basic validations.
StakeRequest storage stakeRequest = stakeRequests[_stakeRequestHash];
require(stakeRequest.staker != address(0), 'Staker request must exists.');
address staker = stakeRequest.staker;
uint256 amount = stakeRequest.amount;
delete stakeRequests[_stakeRequestHash];
address token = GatewayInterface(stakeRequest.gateway).token();
// transfer back the stake amount
require(
EIP20Interface(token).transfer(staker, amount),
"Stake amount must be transferred to staker address."
);
// Emit event
}
function revokeStakeRequest(
bytes32 _stakeRequestHash
)
external
{
StakeRequest storage stakeRequest = stakeRequests[_stakeRequestHash];
require(stakeRequest.staker == msg.sender, 'Only staker can call revokeStakeRequest.');
address staker = stakeRequest.staker;
uint256 amount = stakeRequest.amount;
delete stakeRequests[_stakeRequestHash];
address token = GatewayInterface(stakeRequest.gateway).token();
// transfer back the stake amount
require(
EIP20Interface(token).transfer(staker, amount),
"Stake amount must be transferred to staker address."
);
// Emit event
}
function getNonce(
address _account,
address _gateway
)
external
view
returns (uint256 nonce_)
{
StakerProxy stakerProxy = stakerProxies[_account];
if (address(stakerProxy) == address(0)) {
return 0;
}
nonce_ = GatewayInterface(_gateway).getNonce(_account);
}
}
StakerProxy.sol
pragma solidity ^0.5.0;
import "./GatewayInterface.sol";
import "./EIP20Interface.sol";
contract StakerProxy {
address public composer;
address public owner;
modifier onlyComposer {
require(
msg.sender == composer,
"Msg.sender must be composer address."
);
_;
}
modifier onlyOwner {
require(
msg.sender == owner,
"Msg.sender must be owner address."
);
_;
}
constructor(
address _composer,
address _owner
)
public
{
composer = _composer;
owner = _owner;
}
function acceptStakeRequest(
uint256 _amount,
address _beneficiary,
uint256 _gasPrice,
uint256 _gasLimit,
uint256 _nonce,
bytes32 _hashLock,
address _staker,
address _gateway
)
onlyComposer
external
returns (bytes32 messageHash_)
{
require(owner == _staker, "Staker must be the owner.");
// may be we not this step here.
uint256 nonce = GatewayInterface(_gateway).getNonce(msg.sender);
require(nonce == _nonce, "nonce must be equal.");
// Approve gateway for token transfer.
address token = GatewayInterface(_gateway).token();
EIP20Interface(token).approve(_gateway, _amount);
// Approve gateway for bounty transfer.
address baseToken =GatewayInterface(_gateway).baseToken();
uint256 bounty = GatewayInterface(_gateway).bounty();
EIP20Interface(baseToken).approve(_gateway, bounty);
// This will call stake of gateway contract
messageHash_ = GatewayInterface(_gateway).stake(
_amount,
_beneficiary,
_gasPrice,
_gasLimit,
_nonce,
_hashLock
);
}
}
questions, notes, thoughts
look into multiplexing it for arbitrary gateway
should the stakerProxy be delete-able if wanted by the staker? -- but not the Composer contract
gateway.revert does not need to be implemented
gasprice should be high enough for facilitators to pick up; for now deal with this out-of-contract
Add disable contract functionality that can restrict stakeRequests but the revokeStakeRequest can still work.
OST' Composer
User Story: As a user, I want to deploy my own contracts in the side chain. To do this I need OST ' on the auxiliary chain.
Scenario: As a company/user, I want to write contracts like custom rules. To get OST' on test net we already have
faucet
that can provide the OST'. But formainnet
, it is still a manual/centralized work. Instead of this, it will be helpful for the user if they can stake OST on origin chain and mint OST' on an auxiliary chain.Proposal: “Composer” contract, where (open or whitelisted) facilitator pool can accept (or ignore, and a user can revoke) stakeRequests of OST-> OST’ on
mainnet
; MVP contract + executable (like a faucet)Specifications:
Composer contract: Please note: the contract is referred to
Composer
, but this is not final name. Suggest a better name.Composer
contract will beOrganized
Single
Composer
contract must work for arbitrary gateway.Any address(user) must be able to request stake for OST on origin chain by calling
requestStake
function.Composer should have a function that returns the current
nonce
for the given address that can be used forstakeRequest
Facilitators (whitelisted, for now, later it can be open) can facilitate the stake and mint process on behalf of the user by providing the hash lock. A whitelisted address can call
acceptStakeRequest
to facilitate stake and mint.The staker can revoke the stake request any time unless it has been accepted and processed by the facilitator. Staker can call
revokeStakeRequest()
function to revoke the stake request. Staker can revoke only its own stake requests.A facilitator can reject the stakeRequest by calling
rejectStakeRequest
. We should evaluate if this function is needed. If the facilitator pool will be open then we may not need this function.All events of the composer contracts must be sufficient for the facilitator to decide if they want to accept or ignore the facilitation.
Currently, there is a limitation of one stake request at a time in the Gateway contracts. This must be gracefully handled in the composer contract. One of the proposals is given below that uses proxy contract for staker.
The organization can deactivate the new stake requests. By doing this no one can call stakeRequest. Facilitators can still accept stake requests. Staker can still revoke the stake requests.
StakerProxy contract:
Please note: the contract is referred to
StakerProxy
, but this is not final name. Suggest a better name.Gateways can do one stake - mint process at a time for a given staker address. So the composer contract cannot call gateway.stake() function, it will be not able to serve all stake requests from multiple stakers addresses. So a proxy contract is introduced as a proposal here.
This contract has a minimal code, that gets deployed from the
Composer
contract.It is deployed per user once.
It can be used for arbitrary gateways.
Actual staker is the owner, and it can be destroyed by the owner only.
All other functions can be called only by Composer contract.
This contract can have following variables.
Constructor
Accept stake request
<Ignore this section, for now, this will be updated> Executable (Facilitator):
The executable must be able to support multiple chains simultaneously.
Use mosaic-chains to run chains.
Support multiple workers simultaneously.
Subscribe to
StakeRequested
event.Workers should avoid race on
acceptStakeRequest
; eg round-robinTrack the gas available for each worker, if the worker gas too low to perform any transaction then it must not do facilitation.
Calculate if the
gasPrice
andgasLimit
should be acceptable to do facilitation.It must act as a hunter for any failed facilitation.
Use mosaic.js to interact with Contracts.
To start the facilitator, provide composer address, and chain id
./facilitator start <chainId> <composer_contract_address>
example:
./facilitator start 3 '0xA574BAE4B1853132a35291a853817B8F19928cFD'
Add workers (non interactive) Since the workers will need to do transactions, the worker account must be unlocked. For this we should do it in non-interactive way by providing the password path
Adding Single worker
./facilitator addworker <address> --non-interactive ./password.txt --chain-ids <chainId,...>
Example:
./facilitator addworker '0xA574BAE4B1853132a35291a853817B8F19928cFD' --non-interactive ./password.txt --chain-ids 2 1407 1406
Adding multiple workers
./facilitator addworkers <worker_config_path>
Example:./facilitator addworkers <./worker/path/config.json>
config.json proposal
List workers
./facilitator listworkers <chainid>
List worker balances in all chain ids
./facilitator listworkersbalance
List stakeRequests
./facilitator liststakerequests <chainid>
Handle SIGTERM gracefully
Implementation Proposal
Gateways can do one stake - mint process at a time. Also, the stake function of
gateway
contract is called by thestaker
address.There are multiple ways to implement this
First is like existing gateway composer for branded token. It requires one gateway composer per user. This will require a bookkeeper that tracks all the composers (onchain in contract or off-chain in DB)
The second way is to have a single composer that internally manages the proxy staker account. In this approach,
staker
can callstakeRequest
onComposer
contract. Composer contract internally deploys a proxy contract per user (only once per user). Proxy contract is just used to maintain the nonce and acts as the staker. This proxy contract can be called only by theComposer
contract. An incomplete rough implementation is available here.Composer.sol
StakerProxy.sol
questions, notes, thoughts
Facilitator