include "List.aes"
include "Pair.aes"
namespace Worker =
type package_code = string
record package = {
daily_cap : int,
price : int}
record aggregated_package = {
daily_cap : int,
count : int}
record worker = {
daily_cap : int,
can_withdraw_payout : bool,
packages : map(package_code, aggregated_package),
joined_pool_tmst : int}
record transfer_packs =
{
worker : address,
packages_to_move : list(string * int),
new_address : address
}
datatype approvable_action = Transfer(transfer_packs)
function new_package(price : int, cap : int) : package =
{daily_cap = cap,
price = price}
function claim(ps : list(package_code * (package * int)), joined_tmst : int) : worker =
let daily_cap = daily_cap_from_packs_list(ps)
let packs : map(package_code, aggregated_package) =
List.foldl(
(accum, t) =>
let pack_id = Pair.fst(t)
let (pack, cnt) = Pair.snd(t)
let val =
switch(Map.lookup(pack_id, accum))
None => {daily_cap = pack.daily_cap, count = cnt}
Some(v) => v{count = v.count + cnt}
accum{[pack_id] = val},
{},
ps)
{daily_cap = daily_cap,
can_withdraw_payout = false,
packages = packs,
joined_pool_tmst = joined_tmst}
function split_packages(w : worker, split : transfer_packs) : worker * worker =
let (packages_left, packages_collected) =
List.foldl(
(accum, p) =>
let accum_left = Pair.fst(accum)
let accum_collected = Pair.snd(accum)
let code = Pair.fst(p)
let count = Pair.snd(p)
switch(Map.lookup(code, accum_left))
None => abort("Does not own enough packages")
Some(owned_packs) =>
let left_packs = owned_packs.count - count
require(left_packs > -1, "Does not own enough packages")
(accum_left{[code] = owned_packs{count = left_packs}}, (code, owned_packs{count = count}) :: accum_collected),
(w.packages, []),
split.packages_to_move)
let daily_cap_delta = List.sum(List.map((t) => Pair.snd(t).daily_cap * Pair.snd(t).count, packages_collected))
let new_w = {daily_cap = daily_cap_delta,
can_withdraw_payout = false,
packages = Map.from_list(packages_collected),
joined_pool_tmst = Chain.block_height}
require(Map.size(new_w.packages) == List.length(packages_collected), "Do not split counts of the same package code")
(w{daily_cap = w.daily_cap - daily_cap_delta, packages = packages_left}, new_w)
function merge_workers(w1 : worker, w2: worker) =
let packages =
List.foldl(
(accum, t) =>
let code = Pair.fst(t)
let aggr_pack = Pair.snd(t)
let updated_pack =
switch(Map.lookup(code, accum))
None => aggr_pack
Some(p) => p{count = p.count + aggr_pack.count}
accum{[code] = updated_pack},
w1.packages,
Map.to_list(w2.packages))
// if one of them is allowed to withdraw, so is the resulting new account
let oldest_tmst =
switch(w1.joined_pool_tmst < w2.joined_pool_tmst)
true => w1.joined_pool_tmst
false => w2.joined_pool_tmst
let can_withdraw_payout = w1.can_withdraw_payout || w2.can_withdraw_payout
let daily_cap = daily_cap_from_packages(packages)
{daily_cap = daily_cap,
can_withdraw_payout = can_withdraw_payout,
packages = packages,
joined_pool_tmst = oldest_tmst}
function daily_cap_from_packs_list(ps : list(package_code * (package * int))) =
List.sum(List.map((t) => Pair.fst(Pair.snd(t)).daily_cap * Pair.snd(Pair.snd(t)), ps))
function daily_cap_from_packages(ps : map(package_code, aggregated_package)) =
List.sum(List.map((t) => Pair.snd(t).daily_cap * Pair.snd(t).count, Map.to_list(ps)))
include "Set.aes"
include "List.aes"
include "Pair.aes"
contract interface Data =
stateful entrypoint init : (address) => unit
contract interface Hive =
stateful entrypoint init : (address, address, Data) => unit
entrypoint leader : () => address
stateful entrypoint enroll : (address, Worker.worker) => unit
entrypoint member : (address) => bool
stateful entrypoint remove : (address) => unit
entrypoint get : (address) => Worker.worker
stateful entrypoint set_locked : (bool) => unit
stateful entrypoint set_leader : (address) => unit
entrypoint can_be_destroyed : () => bool
entrypoint info : () => address * string * address * string * string * string * string * list(string)
stateful entrypoint make_payable : (address) => unit
stateful entrypoint make_non_payable : (address) => unit
entrypoint assert_worker_is_payable : (address) => unit
stateful entrypoint force_payout : (address) => unit
stateful entrypoint change_worker_address : (address, address) => unit
entrypoint balance : (address) => int
stateful entrypoint set_data_ct : (Data) => unit
stateful entrypoint move_data_and_coins_to_new_hive : (Hive) => Data
payable entrypoint receive_coins : () => unit
stateful entrypoint evacuate_coins : (int, address) => unit
stateful entrypoint split_packages : (Worker.transfer_packs) => unit
contract interface Multisig =
stateful entrypoint validate : ('a, list(address * signature), int) => unit
main contract HiveMan =
type sig_list = list(address * signature)
record personal_mining_package =
{
codes : map(Worker.package_code, int),
hive : Hive // this is the hive to join once claimed the package
}
datatype event
= PackageTypeAdded(Worker.package_code, int, int)
// | PackageAdded(address, list(Worker.package_code * int))
| NewEurekaAddress(address)
| NewKYCValidatorAddress(address)
| PackageRemoved(address)
| HiveChartered(Hive)
| Enrolled(address, Hive)
| Removed(address, Hive)
| Reassigned(address, Hive, Hive)
| Locked(Hive)
| Unlocked(Hive)
| Withdraw(address, int)
| Destroyed(Hive)
| AdminAdded(address)
| AdminRemoved(address)
| SetLeader(Hive, address)
record state =
{
admins : Set.set(address),
payable_validator : address,
eureka : address,
package_types : map(Worker.package_code, Worker.package),
available_packages : map(address, personal_mining_package),
hives : Set.set(Hive),
pending_admin_approval : map(Worker.approvable_action, Hive),
multisig_enabled : bool,
multisig_ct : Multisig,
master : address,
eureka_can_grant_packages : bool
}
entrypoint init(master: address, eureka : address, payable_validator : address, multisig_ct : Multisig) =
{admins = Set.from_list([Call.origin]),
eureka = eureka,
payable_validator = payable_validator,
package_types = {},
available_packages = {},
hives = Set.new(),
pending_admin_approval = {},
multisig_ct = multisig_ct,
multisig_enabled = false,
master = master,
eureka_can_grant_packages = true
}
// for Eureka
entrypoint leader(hive : Hive) =
assert_hive(hive)
hive.leader()
// for Eureka
stateful entrypoint add_package_type(code : Worker.package_code, price : int, cap : int) : unit =
assert_eureka()
switch(Map.lookup(code, state.package_types))
Some(_) => abort("Package code already created")
None =>
let p = Worker.new_package(price, cap)
Chain.event(PackageTypeAdded(code, price, cap))
put(state{package_types[code] = p})
stateful entrypoint set_eureka(eureka : address, sigs : sig_list) =
assert_consortium_vote(("SET_EUREKA", eureka), sigs, 30)
Chain.event(NewEurekaAddress(eureka))
put(state{eureka = eureka})
stateful entrypoint set_eureka_can_grant_packages(val : bool, sigs : sig_list) =
assert_consortium_vote(("SET_EUREKA_GRANT_PACKAGES", val), sigs, 51)
put(state{eureka_can_grant_packages = val})
entrypoint can_eureka_grant_packages() =
state.eureka_can_grant_packages
stateful entrypoint set_multisig_contract(m : Multisig, sigs : sig_list) =
assert_consortium_vote(("SET_MULTISIG_CONTRACT", m), sigs, 51)
put(state{multisig_ct = m})
entrypoint multisig_contract() =
state.multisig_ct
stateful entrypoint set_master(m : address, sigs : sig_list) =
assert_consortium_vote(("SET_MASTER", m), sigs, 51)
put(state{master = m})
stateful entrypoint set_multisig_enabled(val : bool, sigs : sig_list) =
assert_consortium_vote(("SET_MULTISIG_ENABLED", val), sigs, 51)
put(state{multisig_enabled = val})
entrypoint multisig_enabled() =
state.multisig_enabled
stateful entrypoint set_payable_validator(payable_validator : address, sigs : sig_list) =
assert_consortium_vote(("SET_PAYABLE_VALIDATOR", payable_validator), sigs, 30)
Chain.event(NewKYCValidatorAddress(payable_validator))
put(state{payable_validator = payable_validator})
entrypoint operators() =
(state.admins, state.payable_validator, state.eureka, state.master)
// for Eureka
stateful entrypoint add_package(codes : list(Worker.package_code * int), buyer : address, hive : Hive) : unit =
assert_eureka()
// assert all codes are known
List.foreach(codes,
(t) => switch(Map.lookup(Pair.fst(t), state.package_types))
Some(_) => ()
None => abort("Unknown packgage code"))
let pending_packs =
switch(Map.lookup(buyer, state.available_packages))
Some(p) => p
None => {hive = hive, codes = {}}
// Chain.event(PackageAdded(buyer, codes))
let packs =
List.foldl(
(acc, t) =>
let ix = Pair.fst(t)
acc{[ix = 0] @ c = c + Pair.snd(t)},
pending_packs.codes,
codes)
put(state{available_packages[buyer] = {codes = packs, hive = hive}})
// for Eureka
stateful entrypoint remove_package(buyer : address) : unit =
assert_eureka()
Chain.event(PackageRemoved(buyer))
put(state{available_packages = Map.delete(buyer, state.available_packages)})
stateful entrypoint charter(template_hive_ct : Hive, leader : address, data_template_ct : Data) =
assert_admin()
let hive_ct : Hive = Chain.clone(ref = template_hive_ct, Contract.address, leader, data_template_ct)
let data_ct : Data = Chain.clone(ref = data_template_ct, hive_ct.address)
hive_ct.set_data_ct(data_ct)
Chain.event(HiveChartered(hive_ct))
put(state{hives = Set.insert(hive_ct, state.hives)})
hive_ct
// for Eureka
entrypoint hive_addresses() =
Set.to_list(state.hives)
// for Eureka
entrypoint hives_info() =
List.map((hive) => (hive, hive.info()), Set.to_list(state.hives))
// for Eureka
entrypoint list_package_types() =
state.package_types
// for Eureka
entrypoint lookup_packages(buyer : address) : personal_mining_package =
switch(Map.lookup(buyer, state.available_packages))
None => abort("No available package")
Some(packages) => packages
/* buy a package */
stateful payable entrypoint enroll() =
switch(Map.lookup(Call.origin, state.available_packages))
None => abort("Unknown package")
Some(package) =>
let package_types =
Map.to_list(List.foldl(
(accum, tuple) =>
let code = Pair.fst(tuple)
let count = Pair.snd(tuple)
switch(Map.lookup(code, state.package_types))
None => abort("Unknown packgage code")
Some(t) =>
let old_value =
switch(Map.lookup(code, accum))
Some((_, cnt)) => cnt
None => 0
accum{[code] = (t, old_value + count)},
{},
Map.to_list(package.codes)))
let coins = Call.value
let total_price = List.sum(List.map((t) => Pair.fst(Pair.snd(t)).price * Pair.snd(Pair.snd(t)), package_types))
if(total_price < coins) abort("Too much GAJU")
if(total_price > coins) abort("Insufficient GAJU")
let worker = Worker.claim(package_types, Chain.block_height)
Chain.event(Enrolled(Call.origin, package.hive))
package.hive.enroll(Call.origin, worker)
put(state{available_packages = Map.delete(Call.origin, state.available_packages)})
package.hive
stateful entrypoint grant_packages(codes : list(Worker.package_code * int), receiver : address, hive : Hive) =
grant_packages_(codes, receiver, hive, Chain.block_height)
stateful entrypoint grant_older_packages(codes : list(Worker.package_code * int), receiver : address, hive : Hive, start_height : int) =
grant_packages_(codes, receiver, hive, start_height)
/* this could be really computationally heavy function. Use only in dry-run
context */
// for Eureka
entrypoint member(worker_address : address) =
let res = List.find((hive : Hive) => hive.member(worker_address), Set.to_list(state.hives))
switch(res)
None => abort("Not member in any hive")
Some(hive) =>
hive
/* this can be really computationally heavy function. Use only in dry-run
context */
// for Eureka
entrypoint balances(worker_address : address) =
let res = List.map((hive) => (hive, hive.balance(worker_address)), Set.to_list(state.hives))
List.filter((kv) => Pair.snd(kv) > 0, res)
entrypoint get_worker(worker_address : address, hive : Hive) =
hive.get(worker_address)
/* deletes a worker from a hive forever, there is no going back */
// Currently the accumulated coins are left locked in the contract
stateful entrypoint remove(worker : address, hive : Hive, sigs : sig_list) =
assert_consortium_vote(("REMOVE_MINER", hive, worker), sigs, 30)
assert_hive(hive)
Chain.event(Removed(worker, hive))
hive.remove(worker)
/* Moves a worker from one hive to another */
// Currently the accumulated coins are left in the contract. It is important
// that this does not circumvent the KYC process. Another edge case is a
// worker receiving more coins after they left the hive (rewards do have a
// certain delay). Coins locked in previous contracts can be accessed via
// the remote_payout entrypoint
stateful entrypoint reassign(worker_addr : address, old_hive : Hive, new_hive : Hive) =
assert_admin()
assert_hive(old_hive)
assert_hive(new_hive)
// get the worker with its KYC state
let worker = old_hive.get(worker_addr)
old_hive.remove(worker_addr)
// if the hive is locked, one can not join it
Chain.event(Reassigned(worker_addr, old_hive, new_hive))
new_hive.enroll(worker_addr, worker)
stateful entrypoint lock(hive : Hive, sigs : sig_list) =
Chain.event(Locked(hive))
set_lock(hive, true, sigs)
stateful entrypoint unlock(hive : Hive, sigs : sig_list) =
Chain.event(Unlocked(hive))
set_lock(hive, true, sigs)
stateful entrypoint set_leader(hive : Hive, new_leader : address, sigs : sig_list) =
assert_consortium_vote(("SET_LEADER", hive, new_leader), sigs, 51)
Chain.event(SetLeader(hive, new_leader))
hive.set_leader(new_leader)
/* withdraws coins from the accumulated coins in the contract. Those could
be either coins from sold packages OR withdrawn from hives */
stateful entrypoint withdraw(amount : int, destination : address, sigs : sig_list) =
assert_consortium_vote(("WITHDRAW", amount, destination), sigs, 51)
Chain.event(Withdraw(destination, amount))
Chain.spend(destination, amount)
/* destroys a contract for good. It must be in an appropriate condition to
be destroyed. Any reward received at the hive address after destruction
will be unaccessable forever */
stateful entrypoint destroy(hive: Hive, sigs: sig_list) =
assert_consortium_vote(("DESTROY_POOL", hive), sigs, 51)
destroy_(hive)
stateful entrypoint add_admin(new_admin : address, sigs : sig_list) =
assert_consortium_vote(("ADD_ADMIN", new_admin), sigs, 30)
Chain.event(AdminAdded(new_admin))
put(state{admins = Set.insert(new_admin, state.admins)})
stateful entrypoint rm_admin(admin : address, sigs : sig_list) =
assert_consortium_vote(("REMOVE_ADMIN", admin), sigs, 30)
require(Set.size(state.admins) > 1, "There must be at least one admin left")
Chain.event(AdminRemoved(admin))
put(state{admins = Set.delete(admin, state.admins)})
stateful entrypoint validate_payable(worker_addr : address, hive : Hive) =
assert_payable_validator()
assert_hive(hive)
hive.make_payable(worker_addr)
stateful entrypoint unvalidate_payable(worker_addr : address, hive : Hive) =
assert_payable_validator()
assert_hive(hive)
hive.make_non_payable(worker_addr)
stateful entrypoint remote_payout(old_hive : Hive, new_hive : Hive) =
assert_hive(old_hive)
assert_hive(new_hive)
new_hive.assert_worker_is_payable(Call.origin)
old_hive.force_payout(Call.origin)
/* this function is reserved only for an escape hatcher when a worker had lost
access to their private keys. This function is intended for an admin to
rename the whole account and exsisting balances stored in the contract and
to contribute them to the new account pubkey */
stateful entrypoint change_worker_address(old_addr : address, new_addr : address, hive : Hive, sigs : sig_list) =
assert_consortium_vote(("RENAME", hive, old_addr, new_addr), sigs, 51)
assert_hive(hive)
hive.change_worker_address(old_addr, new_addr)
/* migrates all workers from old_hive and moves them with their balances to
new_hive. Should be used in the case of migration from one contract to the
other */
stateful entrypoint migrate_data_between_hives(old_hive : Hive, new_hive : Hive, sigs : sig_list) =
assert_consortium_vote(("MIGRATE_DATA_BETWEEN_POOL", old_hive, new_hive), sigs, 51)
assert_hive(old_hive)
assert_hive(new_hive)
let data_ct = old_hive.move_data_and_coins_to_new_hive(new_hive)
new_hive.set_data_ct(data_ct)
destroy_(old_hive)
stateful entrypoint evacuate_hive_coins(hive : Hive, amount : int, safeheaven : address, sigs : sig_list) =
assert_consortium_vote(("EVACUATE_COINS", hive, amount, safeheaven), sigs, 51)
assert_hive(hive)
hive.evacuate_coins(amount, safeheaven)
stateful entrypoint apply_to_admin(action : Worker.approvable_action, hive : Hive) =
let worker_addr = action_worker(action)
require(worker_addr == Call.origin, "Only the owner can apply")
assert_hive(hive)
require(hive.member(worker_addr), "Not a member in that hive")
// maybe check if one has enough packages to split?
put(state{pending_admin_approval[action] = hive})
stateful entrypoint approve(a : Worker.approvable_action) =
assert_admin()
switch(Map.lookup(a, state.pending_admin_approval))
None => abort("Unknown apply action")
Some(hive) =>
let worker_addr = action_worker(a)
assert_hive(hive)
require(hive.member(worker_addr), "Not a member in that hive") // maybe delegate this to the hive call?
switch(a)
Worker.Transfer(split) =>
hive.split_packages(split)
put(state{pending_admin_approval = Map.delete(a, state.pending_admin_approval)})
stateful entrypoint decline(action : Worker.approvable_action) =
assert_admin()
put(state{pending_admin_approval = Map.delete(action, state.pending_admin_approval)})
entrypoint pending_for_admin_approval() =
state.pending_admin_approval
// private functions
function assert_admin() =
require(Set.member(Call.origin, state.admins), "Caller must be an admin")
function assert_hive(hive : Hive)=
require(Set.member(hive, state.hives), "Unknown hive")
function assert_eureka() =
require(Call.origin == state.eureka, "This call is reserved for Eureka")
function assert_payable_validator() =
require(Call.origin == state.payable_validator, "This call is reserved for the payable validator")
function map_keys(map) =
List.map((kv) => Pair.fst(kv), Map.to_list(map))
stateful function set_lock(hive : Hive, val : bool, sigs : sig_list) =
assert_consortium_vote(("SET_LOCK", hive, val), sigs, 30)
switch(Set.member(hive, state.hives))
false => abort("Unknown hive")
true =>
hive.set_locked(val)
function action_worker(a) =
switch(a)
Worker.Transfer(split) => split.worker
function assert_consortium_vote(msg, sigs, threshold) =
switch(state.multisig_enabled)
false => require(Call.origin == state.master, "This call is reserved for master address")
true => state.multisig_ct.validate(msg, sigs, threshold)
stateful function destroy_(hive : Hive) =
assert_hive(hive)
require(hive.can_be_destroyed(), "The hive is not ready to be destroyed")
Chain.event(Destroyed(hive))
put(state{hives = Set.delete(hive, state.hives)})
stateful function grant_packages_(codes : list(Worker.package_code * int), receiver : address, hive : Hive, start_height : int) =
assert_eureka()
assert_hive(hive)
require(state.eureka_can_grant_packages, "Eureka package granting is disabled")
let ps =
List.map(
(t) =>
let (code, count) = t
switch(Map.lookup(code, state.package_types))
Some(p) => (code, (p, count))
None => abort("Unknown packgage code"),
codes)
let worker = Worker.claim(ps, start_height)
Chain.event(Enrolled(receiver, hive))
hive.enroll(receiver, worker)
hive