Spankbank
Algorithmic central bank that powers the two-token SpankChain economic system.
Install / Use
/learn @SpankChain/SpankbankREADME
SpankBank
The SpankBank is an algorithmic central bank that powers the two-token SpankChain economic system.
-
SPANK is a staking token which can be deposited with the SpankBank to earn newly minted BOOTY.
-
BOOTY is low-volatility fee credit good for $1 worth of SpankChain services.
SpankChain will collect fees for using the camsite, payment hub, advertising network, and other services in BOOTY. The fees are sent to the SpankBank where they are counted and burned.
The SpankBank has a 30 day billing period. At the beginning of each new period, if the total BOOTY supply is under the target supply (20x the total fees collected in the previous period), new BOOTY is minted to reach the target supply and distributed proportionally to all SPANK stakers.
In the future, we plan to add features to incentivize decentralized moderation of the SpankChain platform, rewarding BOOTY to those who help maintain its integrity. We also plan to add mechanisms that will incentivize maintaining the $1 BOOTY peg.
Usage
SPANK holders can stake their SPANK tokens with the SpankBank for up to 12 (30 day) periods by calling the stake function. BOOTY fees can be paid to the SpankBank by anyone by calling the sendFees function. When a new period starts, anyone can mint BOOTY for the previous period by calling the mintBooty function. Once BOOTY has been minted for a previous period, the stakers for that period can call the claimBooty function to claim their BOOTY. In order to be eligible to receive BOOTY, during each staking period stakers have to check in with the SpankBank by calling the checkIn function. Stakers can optionally extend their stake for additional periods when checking in. When a staker's stake has expired, they can withdraw their staked SPANK using the withdrawStake function.
If stakers want to only partially extend their stake (e.g. extend only 50% of their stake by an additional month, not all of it) or to transfer some of their stake to a new address (e.g. for security reasons) they can do so by calling the splitStake function.
If stakers want to close the SpankBank and be able to withdraw early (e.g. in case of catastrophic bug or planned upgrade), they can call the voteToClose function. If stakers accounting for more than 50% of the staked SPANK call voteToClose in the same period, the SpankBank will immediately transition to a "closed" state and allow stakers to withdraw early.
Data Structures
Global Constants
uint256 public periodLength; - time length of each period in seconds (30 days = 2592000 seconds)
uint256 public maxPeriods; - maximum number of staking periods (12)
HumanStandardToken public spankToken; - the SPANK token contract reference
MintableToken public bootyToken; - the BOOTY token contract reference
SpankPoints
Stakers are rewarded with extra BOOTY for staking for additional periods. The SpankPoints for each staker are calculated as the amount of SPANK staked multiplied by the staking factor. The staking factor ranges from 100% if staking for the maximum length of 12 periods, to 45% for staking the minimum length of 1 period.
// LOOKUP TABLE FOR STAKING FACTOR BY PERIOD
// 1 -> 45%
// 2 -> 50%
// ...
// 12 -> 100%
mapping(uint256 => uint256) pointsTable;
In order to earn the maximum BOOTY, a staker would need to stake for 12 periods, and then opt to extend their stake by 1 period during every check in.
If a staker stakes for 12 periods but doesn't opt to extend their stake during check ins, they would receive 100% of the BOOTY for the first period, 95% for the second period, and so forth until they receive 45% during the final period.
Internal Accounting
uint256 public currentPeriod = 0; - the current period number
uint256 public totalSpankStaked; - the total SPANK staked across all stakers
bool public isClosed; - true if voteToClose has passed, allows early withdrawals
Stakers
The Staker struct stores all relevant data for each staker, and is saved in the stakers mapping by the staker's address.
struct Staker {
uint256 spankStaked; // the amount of spank staked
uint256 startingPeriod; // the period this staker started staking
uint256 endingPeriod; // the period after which this stake expires
mapping(uint256 => uint256) spankPoints; // the spankPoints per period
mapping(uint256 => bool) didClaimBooty; // true if staker claimed BOOTY for that period
mapping(uint256 => bool) votedToClose; // true if staker voted to close for
that period
address delegateKey; // address used to call checkIn and claimBooty
address bootyBase; // destination address to receive BOOTY
}
mapping(address => Staker) public stakers;
The staker.spankPoints mapping stores the staker's spank points for each period. The staker.didClaimBooty mapping tracks whether or not the staker has claimed their BOOTY for each period.
Periods
The Period struct stores all relevant data for each period, and is saved in the periods mapping by the period number.
struct Period {
uint256 bootyFees; // the amount of BOOTY collected in fees
uint256 totalSpankPoints; // the total spankPoints of all stakers
uint256 bootyMinted; // the amount of BOOTY minted
bool mintingComplete; // true if BOOTY has already been minted for this period
uint256 startTime; // the starting unix timestamp in seconds
uint256 endTime; // the ending unix timestamp in seconds
uint256 closingVotes; // the total votes to close this period
}
mapping(uint256 => Period) public periods;
The data for each period is set in the following order:
- The
totalSpankPointsare tallied during the previous period, as each staker calls thestakeorcheckInfunctions. - The
startTimeandendTimeare set when the period starts, when theupdatePeriodfunction is called. - The
bootyFeesare tallied during the period, as thesendFeesfunction is called. - The
closingVotesare tallied during the period, as stakers callvoteToClose. - Once the period is over,
bootyMintedandmintingCompleteare set when themintBootyfunction is called during the next period.
Functions
SpankBank Constructor
- Saves the passed in
periodLengthandmaxPeriodsas global constants. - Builds and saves the SPANK token reference from the passed in
spankAddress. - Deploys the BOOTY token contract and mints
initialBootySupplyBOOTY tokens. - Transfers all newly minted BOOTY to the
msg.sender. - Immediately starts the first period (period 0) at
startTime = now. - Set the
endTimeof the first period to 30 days fromnow. - Initialize the
pointsTablewith hard coded values.
constructor (
uint256 _periodLength,
uint256 _maxPeriods,
address spankAddress,
uint256 initialBootySupply,
string bootyTokenName,
uint8 bootyDecimalUnits,
string bootySymbol
) public {
periodLength = _periodLength;
maxPeriods = _maxPeriods;
spankToken = HumanStandardToken(spankAddress);
bootyToken = new MintAndBurnToken(bootyTokenName, bootyDecimalUnits, bootySymbol);
bootyToken.mint(this, initialBootySupply);
uint256 startTime = now;
periods[currentPeriod].startTime = startTime;
periods[currentPeriod].endTime = SafeMath.add(startTime, periodLength);
bootyToken.transfer(msg.sender, initialBootySupply);
// initialize points table
pointsTable[0] = 0;
pointsTable[1] = 45;
pointsTable[2] = 50;
pointsTable[3] = 55;
pointsTable[4] = 60;
pointsTable[5] = 65;
pointsTable[6] = 70;
pointsTable[7] = 75;
pointsTable[8] = 80;
pointsTable[9] = 85;
pointsTable[10] = 90;
pointsTable[11] = 95;
pointsTable[12] = 100;
emit SpankBankCreated(_periodLength, _maxPeriods, spankAddress, initialBootySupply, bootyTokenName, bootyDecimalUnits, bootySymbol);
}
Bootstrapping BOOTY
The initial BOOTY balance is sent to the msg.sender to be distributed to all
period 0 stakers through a token airdrop. The airdrop will take place at the end
of period 0 (30 days from SpankBank deployment) and will be based on each
staker's spankPoints for period 1.
updatePeriod
In order to make sure all interactions with the SpankBank take place during the correct period, the updatePeriod function is called at the beginning of every state-updating function.
So long as the current time (now) is greater than the endTime of the current period (meaning the period is over), the currentPeriod is incremented by one and then the startTime and the endTime for the next Period are set as well.
function updatePeriod() public {
while (now >= periods[currentPeriod].endTime) {
Period memory prevPeriod = periods[currentPeriod];
emit PeriodEvent(
currentPeriod,
prevPeriod.bootyFees,
prevPeriod.totalSpankPoints,
prevPeriod.bootyMinted,
prevPeriod.closingVotes
);
currentPeriod += 1;
periods[currentPeriod].startTime = prevPeriod.endTime;
periods[currentPeriod].endTime = SafeMath.add(prevPeriod.endTime, periodLength);
}
}
The reason this is done using a while loop is just in case an entire period passes without any SpankBank interactions taking place. This is extremely unlikely and would mean no fees were paid not a single staker checked in, but we wanted to protect against that scenario anyways.
One scenario the updatePeriod function does not protect against is if enough periods pass without any SpankBank interactions that in order to catch up to the current period the while loop has to run e
