This repo contains token as well as protocol related smart contracts used in Marlin.
- Spin up an instance for running or testing the contracts.
- Make sure you have docker installed and execute the following command:
sh start.sh
- If you are not using docker, make sure you have truffle, node 10.x installed on your device.
- Use the following commands to compile and test the contracts.
truffle compile;
ganache-cli -a 51;
truffle test;
POND is a standard ERC-20 token contract with a total supply of 10 billion.
MPOND is Marlin's governance and staking token. It can used to create and vote on proposals. It is also required to run a Marlin node.
- Total supply of MPOND is 10,000.
- MPOND can be used to vote and create proposals, where 1 MPOND tokens = 1 vote (votes are fungible as tokens).
- MPOND tokens delegated to other users will be locked.
- Direct MPOND transfers will be locked except for whitelisted addresses (like the bridge and stakedrop contracts). Until universal transfers are enabled, only transfers to/from whitelisted addresses are possible.
- MPOND can be converted to POND via the bridge.
- If delegated, users will first have to undelegate the tokens before being able to make a transfer.
Notable functions of the contract are described below:
addWhiteListAddress**(address _address)
adds a new address that is whitelisted for transfers and can only be invoked by admin.enableAllTransfers()
enables anyone to transfers MPOND and can be only invoked by admin.balanceOf
,transfer
,approve
,transferFrom
functions, have same signatures as that of standard ERC20 Token contract- Can delegate tokens to any address using
delegate(address delegatee, uint96 amount)
wheredelegatee
is the address of the delegatee andamount
is number of token to be delegated. - To undelegate the tokens from any address use
undelegate(address delegatee, uint96 amount)
. getCurrentVotes(address account)
returns the current votes that have been delegated toaccount
.getPriorVotes(address account, uint256 blockNumber)
returns the votes the delegated toaccount
at a givenblockNumber
.
- The governance contracts will be able to create any number of proposals.
- Any users who has 1 or more delegated MPOND should be able to successfully create a proposal.
- An address can only have 1 Active proposal at a time.
- A proposal when created is in a Pending state by default. After 2 days it changes to an Active state.
- Any address with MPOND, owned or delegated, can cast vote on any proposal. The number of votes an address is eligible for depends on its MPOND balance at the time of creation of the proposal.
- The voting period is open for 3 days.
- If the proposal has gathered atleast 8000 MPOND votes in 3 days, the state changes from Active to Succeeded.
- If the proposal fails to gather sufficient votes, the state changes to Defeated
- Succeeded can be changed to Queued state by a contract call.
- A Queued proposal will be locked for 2 days. Within this time, the admin has right to Reject the proposal. If Unrejected, the proposal is Executed.
Governance comprises of three contracts
- MPOND token contract
- GovernorAlpha
- Timelock
Token contract as described above.
- A proposal can be made by holding 1 MPOND (which is also equivalent to vote) using the method
function propose(address[] memory targets, uint256[] memory values, string[] memory signatures, bytes[] memory calldatas, string memory description)
wheretargets
refer to the target contracts that need to be updatedvalues
,signature
,calldata
correspond to functions being calleddescription
is an optional field that can be used to define the proposal
- When in Active state, a proposal can be voted on using method
castVote(uint256 proposalId, bool support)
whereproposalId
is the id of the proposalsupport
is the boolean parameter wheresupport=true
indicates that the address has voted for the proposal
- A Succeeded Proposal can be passed to Queued state. When in Queued state it can be rejected by the admin within 3 days. To queue a proposal, call function
queue(uint256 proposalId)
whereproposalId
is the id of the proposal. - A Queued Proposal can be executed using
execute(uint256 proposalId)
whereproposalId
is the id of the existing proposal.
Timelock contract is used for locking the Proposal in various stage of its cycle. It is used internally within GovernorAlpha.
The stake-drop happens via set of 3 oracles and a distribution contract. The purpose of the whole set of contracts is to distribute the mPond token to all registered-delegators who have been staking with the whitelisted validators.
- AddressRegistry.sol
- ValidatorRegistry.sol
- StakeRegistry.sol
- Distribution.sol
All the oracles are derived from a contract StandardOracle.sol. This act as a address based access registry for all the child contracts.
- Contains the mapping of addresses that have to access to pump the data to the contract. Mapping is defined as
mapping(address => bool) sources
- The address that deploys the contract is a
source
by default - Only an existing source can add a new source using method
addSource(address _newSource)
where_newSource
is the address of the new source - A source can renounce itself using method
renounceSource()
ValidatorRegistry is derived from StandardOracle
- Contains the mapping of whitelisted validators for every era in
mapping(uint256 => mapping(bytes32 => bool)) validators
i.e.
isValidator = validators[era][validatorAddressHash]
//isValidator will be true if the particular address hash is validator in that era
- Validator Address's hash is used in the mapping, not the address itself. The hash can be computed using
validatorAddressHash = web3.utils.keccak256(stakingAddress)
- Validators can be adding using
addValidator(uint256 epoch, bytes32 validatorAddressHash)
whereepoch
is the epoch/era number of the staking chainvalidatorAddressHash
is the hash of the validator's address
- Validator's can also be added in bulk using
addValidatorsBulk(uint256 epoch, bytes32[] memory validators)
whereepoch
is the epoch/era number of the staking chainvalidators
array is the list of validators in the array.- Note:
addValidatorsBulk
will revert if even one of the validatorHash in the array is repeated
- After adding validators, the list has to be frozen. This is prevent any further adding of validators accidentally. This is done via
freezeEpoch(uint256 epoch)
whereepoch
is the epoch/era number of the staking chain
- Whenever a validator is successfully added
AddValidator**(uint256 indexed, bytes32 indexed)
event is emitted whereuint256
, the first indexed param of event, isepoch
number of the staking chainbytes32
, the second indexed param of event, is thekeccak256
hash of the validator address.
AddressRegistry is derived from StandardOracle
- Contains mapping of delegator's address hash and equivalent ethereum address.
mapping(bytes32 => address) addressList
,- where
bytes32
is thekeccak256
hash of the delegator'saddress
- where
- Also contains ethereum address and it's equivalent delegator's address hash
mapping(address => bytes32) reverseMap
,- where
bytes32
is thekeccak256
hash of the delegator'saddress
- where
- Address can added via one of the
source
address of the oracle usingaddAddress(bytes32 stakingAddressHash, address ethereumAddress)
- where
stakingAddressHash
is thekeccak256
hash of the delegator'sethereumAddress
- where
- After successfull adding, event
AddressRegistered(bytes32 indexed, address indexed)
is emited wherebytes32
, first param of the event, is the hash of the delegator'sstakingAddress
address
. second param of the event, is the eth/reward address of the delegator
- Address can be removed using
removeAddress**(bytes32 stakingAddressHash)
- where
stakingAddressHash
is thekeccak256
is the hash of the delegator stakingaddress
- where
StakeRegistry is derived from StandardOracle
- Stakes can be added only after validators list have been frozen the ValidatorRegistry contract
- Stakes can be added only after total stakes in the era have been updates. Method
addTotalStakeForEpoch(uint256 epoch, uint256 amount)
- where
epoch
is the epoch/era number of the respective chain amount
is the total stake put in that era
- where
- Stakes can be added using method
addStake(uint256 epoch, bytes32 stakingAddressHash, bytes32 validatorAddressHash, uint256 amount)
- where
epoch
is the epoch/era number of the respective chain - where
stakingAddressHash
is thekeccak256
hash of the delegator's address validatorAddressHash
is thekeccak256
hash of validator's addressamount
is the delegator's stake
- where
- Stakes can be added in bulk using
addStakeBulk(uint256 epoch, bytes32[] memory stakingAddresses, bytes32[] memory validatorAddresses, uint256[] memory amounts)
- where
epoch
is the epoch/era number of the respective chain - where
stakingAddresses
is the array ofkeccak256
hashes of stakingAddresses of delegators - where
validatorAddresses
is the array ofkeccak256
hashes of validators, matched against the arity of thestakingAddresses
- where
amounts
is the array of delegator's stake - Note: Only stakes which are delegated to whitelisted validators are rewarded.
- where
- On successful adding of stake
StakeAdded(uint256, bytes32, bytes32, uint256)
event is emitted whereuint256
, first parameter of the event, is epoch/era number corresponding to the staking chainbytes32
, the second parameter of the event, is the delegator staking addresskeccak256
hashbytes32
, thrid paramter of the event, is the validator's address hash corresponding w,r,t arity match.uint256
, the fourth paramter of the event, is the delegators stake in that era/epoch
- If the validatorAddress is not whitelisted, adding the stake is skipped. Event
StakeSkipped(uint256, bytes32, bytes32, uint256)
event is emitted. The event is similar toStakeAdded
rewardPerEpoch
is the total reward that is distributed to all delegators. This can only be changed by governance.mapping(bytes32 => uint256) rewardPerAddress
stores the reward that has to dispensed to every user wherebytes32
refers to thekeccak256
hash of the delegators staking addressuint256
is the reward earned that corresponding address hash
The contract depends on the above three oracles.
mapping(bytes32 => uint256) claimedBalances
contains the balances withdrawn the delegator's address hash, wherebytes32
refers to the delegator's address hashuint256
refers to the claimed/withdrawn balance
getUnclaimedAmount()
returns the available balance that can be dispensed. The function fetches the address hash fromAddressRegistry.sol
contract, where bidirectional map ofaddress hash
andreward address
are storedclaimAmount()
transfers the reward tokens to the user.
Marlin uses POND and MPOND tokens as part its token economy.
POND is a simple ERC20 token which is transferable and can be delegated to Marlin nodes. Network rewards for work done by validators is received in POND.
MPOND is initially non-transferable between users. They are used in governance and are also required to run nodes.
A bridge contract is used to convert between MPOND and POND. 1 MPOND can exchanged for 1 million POND tokens and vice-versa via the bridge.
POND can be converted to MPOND by sending POND to the bridge and an equivalent number of MPOND (#POND/1m) is received on the same address while burning the POND tokens sent.
The bridge also allows conversion of MPOND to an equivalent number of POND (#POND X 1m). However, the conversion is a bit nuanced and not instantaneous as above. The mechanism is described below.
A request can be made on the bridge to convert a certain number of MPOND (say P). After transfer request is made, there is a wait time of W blocks (set at approximately 6 months initially) before a conversion can be attempted. During the wait time, MPOND can still be used towards staking and governance. After the wait time, a fraction L of P MPOND can be sent to the bridge for conversion.
Parameter W and L are both controlled by governance. These parameters make sure that there are always enough MPOND locked to ensure that the security of the network and its governance is not compromised. After every W blocks,
A series of scenarios and expected results of calls made to the Bridge are illustrated in the table below.
Timespan | MPOND balance | Call Maxima | Result | Maxima mapping | Maxima used mapping | Liquidity | Effective Liq. (calculated) | Call convert | Result |
---|---|---|---|---|---|---|---|---|---|
Day -1 | 1000 | {-1:0} | 0% | ||||||
Day 0 | 1000 | 1100 | reject | {-1:0} | 0% | ||||
Day 0 | 1000 | 900 | accept | {0: 900} | {0:0} | 0% | |||
Day 30 | 1000 | 50 | accept | {0: 900, 30: 50} | {0:0, 30:0} | 0% | |||
Day 31 | 1000 | 100 | reject | {0: 900, 30: 50} | |||||
Day 180 | 1000 | {0: 900, 30: 50} | {0:0, 30:0} | 0% | 950, 0 | reject | |||
Day 180 | 1000 | {0: 900, 30: 50} | {0:0, 30:0} | 10% | |||||
Day 180 | 1000 | {0: 900, 30: 50} | {0:0, 30:0} | 10% | 85, 0 | accept | |||
Day 180 | 915 | {0: 900, 30: 50} | {0:85, 30:0} | 10% | 10, 0 | reject | |||
Day 180 | 915 | {0: 900, 30: 50} | {0:85, 30:0} | 5% | 10, 0 | reject | |||
Day 180 | 915 | {0: 900, 30: 50} | {0:85, 30:0} | 10% | 10, 0 | reject | |||
Day 180 | 915 | {0: 900, 30: 50} | {0:85, 30:0} | 10% | 2, 0 | accept | |||
Day 180 | 913 | {0: 900, 30: 50} | {0:87, 30:0} | 10% | |||||
Day 210 | {0: 900, 30: 50} | {0:87, 30:0} | 10% | 10, 30 | reject | ||||
Day 210 | {0: 900, 30: 50} | {0:87, 30:0} | 20% | 10, 30 | accept | ||||
Day 211 | {0: 900, 30: 50} | {0:87, 30:10} | 20% | 100, 0 | reject | ||||
Day 212 | {0: 900, 30: 50} | {0:87, 30:10} | 20% | 20% | 93, 0 | accept | |||
Day 213 | {0: 900, 30: 50} | {0:180, 30:10} | 20% | 20% | |||||
Day 360 | {0: 900, 30: 50} | {0:180, 30:10} | 20% | 40% | |||||
Day 390 | {0: 900, 30: 50} | {0:180, 30:10} | 15% | 30% |
- POND can be instantly converted into MPOND with 1MPOND yielded against 10^6 POND.
- To convert MPOND to POND, there is a delay of atleast W blocks.
- At any point,
$min(100, liquidityRatio*floor[(time since request)/W])$ % of the total requested amount including all previous conversions for the request can be transferred to POND. - If POND/MPOND are staked/delegated, then they can’t be transferred to the bridge.
- During wait period, MPOND can be used for governance and staking.
- User can partially/fully cancel conversion requests from MPOND to POND at any time, even after wait time is over as long as the conversion is not completed.
- Current conversion requests and their details should be efficiently retrievable.
- Bridge contract should be upgradable.