diff --git a/interface.rsh b/interface.rsh index 0744590..380dc00 100644 --- a/interface.rsh +++ b/interface.rsh @@ -3,8 +3,7 @@ // ----------------------------------------------- // Name: KINN Active Reverse Auction (A1) -// Author: Nicholas Shellabarger -// Version: 1.2.3 - add bid fee and unlock api +// Version: 1.2.8 - updat stake add delegate // Requires Reach v0.1.11-rc7 (27cb9643) or later // ----------------------------------------------- // TODO calculate price change per second with more precision @@ -13,17 +12,87 @@ import { min, max } from "@nash-protocol/starter-kit#lite-v0.1.9r1:util.rsh"; +import { + view, + baseEvents, + baseState +} from "@KinnFoundation/base#base-v0.1.11r4:interface.rsh"; + +import { + rStake, + rUnstake +} from "@KinnFoundation/stake#stake-v0.1.11r1:interface.rsh"; + +import { + Params, + State as ReverseState, + MContract +} from "@KinnFoundation/reverse#reverse-v0.1.11r3:interface.rsh"; + // CONSTS const SERIAL_VER = 0; // serial version of reach app reserved to release identical contracts under a separate plana id -const DIST_LENGTH = 8; // number of slots to distribute proceeds after sale +const DIST_LENGTH = 10; // number of slots to distribute proceeds after sale + +const FEE_MIN_ACTIVE_BID = 1; // 1au +const FEE_MIN_ACTIVE_ACTIVATION = 1; // 1au + +/* + * namedd indices for addrs + */ +const ADDR_RESERVED_ACTIVE_BIDDER = 0; +const ADDR_RESERVED_CURATOR = 1; +const ADDR_RESERVED_ADDR = 2; + +// TYPES + +const ActiveState = Struct([ + ["activeToken", Token], + ["activeAmount", UInt], + ["activeCtc", Contract], +]); + +const State = Struct([ + ...Struct.fields(ReverseState(DIST_LENGTH)), + ...Struct.fields(ActiveState), +]); + +// FUN + +const fState = (State) => Fun([], State); +export const fTouch = Fun([], Null); +export const fAcceptOffer = Fun([Address], Null); +export const fCancel = Fun([], Null); +export const fBid = Fun([Contract], Null); +export const fBidCancel = Fun([], Null); + +// REMOTE FUN + +export const rState = (ctc, State) => { + const r = remote(ctc, { state: fState(State) }); + return r.state(); +}; -const FEE_MIN_ACCEPT = 9_000; // 0.006 -const FEE_MIN_CONSTRUCT = 7_000; // 0.005 -const FEE_MIN_RELAY = 1_7000; // 0.017 -const FEE_MIN_CURATOR = 10_000; // 0.1 -const FEE_MIN_BID = 1_000; // 0.001 +export const rTouch = (ctc) => { + const r = remote(ctc, { touch: fTouch }); + r.touch(); +}; + +export const rBid = (ctc) => { + const r = remote(ctc, { bid: fBid }); + r.bid(); +}; + +// API + +export const api = { + touch: fTouch, + acceptOffer: fAcceptOffer, + cancel: fCancel, + bid: fBid, + bidCancel: fBidCancel, +}; // FUNCS @@ -35,7 +104,6 @@ const precision = 1000000; // 10 ^ 6 /* * calculate price based on seconds elapsed since reference secs */ - const priceFunc = (secs) => (startPrice, floorPrice, referenceConcensusSecs, dk) => max( @@ -76,87 +144,38 @@ const safePercent = (amount, percentage, percentPrecision) => // INTERACTS +const auctioneerInteract = { + getParams: Fun([], Params(DIST_LENGTH)), +}; + const relayInteract = {}; -const Params = Object({ - tokenAmount: UInt, // NFT token amount - startPrice: UInt, // 100 - floorPrice: UInt, // 1 - endSecs: UInt, // 1 - addrs: Array(Address, DIST_LENGTH), // [addr, addr, addr, addr, addr, addr, addr, addr, addr, addr] - distr: Array(UInt, DIST_LENGTH), // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - royaltyCap: UInt, // 10 - acceptFee: UInt, // 0.008 - constructFee: UInt, // 0.006 - relayFee: UInt, // 0.007 - curatorFee: UInt, // 0.1 - bidFee: UInt, // 0.001 -}); +const eveInteract = {}; -const auctioneerInteract = { - getParams: Fun([], Params), - signal: Fun([], Null), -}; +// CONTRACT -export const Event = () => []; +export const Event = () => [Events({ ...baseEvents })]; export const Participants = () => [ - Participant("Auctioneer", auctioneerInteract), - ParticipantClass("Relay", relayInteract), + Participant("Manager", auctioneerInteract), + Participant("Relay", relayInteract), + Participant("Eve", eveInteract), ]; -const State = Struct([ - ["manager", Address], - ["token", Token], - ["tokenAmount", UInt], - ["currentPrice", UInt], - ["startPrice", UInt], - ["floorPrice", UInt], - ["closed", Bool], - ["endSecs", UInt], - ["priceChangePerSec", UInt], - ["addrs", Array(Address, DIST_LENGTH)], - ["distr", Array(UInt, DIST_LENGTH)], - ["royaltyCap", UInt], - ["who", Address], - ["partTake", UInt], - ["acceptFee", UInt], - ["constructFee", UInt], - ["relayFee", UInt], - ["curatorFee", UInt], - ["curatorAddr", Address], - ["timestamp", UInt], - ["activeToken", Token], - ["activeAmount", UInt], - ["activeAddr", Address], -]); +export const Views = () => [View(view(State))]; -export const Views = () => [ - View({ - state: State, - }), -]; - -export const Api = () => [ - API({ - touch: Fun([], Null), - acceptOffer: Fun([Address], Null), - cancel: Fun([], Null), - bid: Fun([UInt], Null), - unlock: Fun([], Null), - }), -]; +export const Api = () => [API(api)]; export const App = (map) => { const [ { amt, ttl, tok0: token, tok1: activeToken }, [addr, _], - [Auctioneer, Relay], + [Manager, Relay, _], [v], [a], - _, + [e], ] = map; - Auctioneer.only(() => { + Manager.only(() => { const { tokenAmount, startPrice, @@ -165,28 +184,17 @@ export const App = (map) => { addrs, distr, royaltyCap, - acceptFee, - constructFee, - relayFee, - curatorFee, - bidFee, } = declassify(interact.getParams()); }); - // Step - Auctioneer.publish( + Manager.publish( tokenAmount, startPrice, floorPrice, endSecs, addrs, distr, - royaltyCap, - acceptFee, - constructFee, - relayFee, - curatorFee, - bidFee + royaltyCap ) .check(() => { check(tokenAmount > 0, "tokenAmount must be greater than 0"); @@ -204,30 +212,11 @@ export const App = (map) => { royaltyCap == (10 * floorPrice) / 1000000, "royaltyCap must be 10x of floorPrice" ); - check( - acceptFee >= FEE_MIN_ACCEPT, - "acceptFee must be greater than or equal to minimum accept fee" - ); - check( - constructFee >= FEE_MIN_CONSTRUCT, - "constructFee must be greater than or equal to minimum construct fee" - ); - check( - relayFee >= FEE_MIN_RELAY, - "relayFee must be greater than or equal to minimum relay fee" - ); - check( - curatorFee >= FEE_MIN_CURATOR, - "curatorFee must be greater than or equal to minimum curator fee" - ); - check( - bidFee >= FEE_MIN_BID, - "bidFee must be greater than or equal to minimum bid fee" - ); }) .pay([ - amt + (constructFee + acceptFee + relayFee + curatorFee) + SERIAL_VER, + amt + SERIAL_VER, [tokenAmount, token], + [FEE_MIN_ACTIVE_ACTIVATION, activeToken], ]) .timeout(relativeTime(ttl), () => { // Step @@ -235,9 +224,11 @@ export const App = (map) => { commit(); exit(); }); - transfer(amt + constructFee + SERIAL_VER).to(addr); + transfer([amt + SERIAL_VER, [FEE_MIN_ACTIVE_ACTIVATION, activeToken]]).to( + addr + ); - Auctioneer.interact.signal(); + e.appLaunch(); const distrTake = distr.sum(); @@ -250,35 +241,26 @@ export const App = (map) => { ).i.i; const initialState = { - manager: Auctioneer, + ...baseState(Manager), token, tokenAmount, currentPrice: startPrice, startPrice, floorPrice, - closed: false, endSecs, priceChangePerSec: dk / precision, - addrs, + addrs: Array.set(addrs, ADDR_RESERVED_ADDR, addr), distr, royaltyCap: royaltyCap, - who: Auctioneer, - partTake: 0, - acceptFee, - constructFee, - relayFee, - curatorFee, - curatorAddr: Auctioneer, + who: Manager, timestamp: referenceConcensusSecs, activeToken, activeAmount: 0, - activeAddr: Auctioneer, + activeCtc: getContract(), // ref to self never used }; - v.state.set(State.fromObject(initialState)); - // Step - const [state] = parallelReduce([initialState]) + const [state, mctc] = parallelReduce([initialState, MContract.None()]) .define(() => { v.state.set(State.fromObject(state)); }) @@ -292,25 +274,28 @@ export const App = (map) => { "token balance accurate after closed" ) // ACTIVE TOKEN BALANCE - .invariant( - implies(!state.closed, balance(activeToken) == state.activeAmount), - "active token balance accurate before closed" - ) - .invariant( - implies(state.closed, balance(activeToken) == 0), - "active token balance accurate before closed" - ) + .invariant(balance(activeToken) == 0, "active token balance accurate") // BALANCE .invariant( - implies(!state.closed, balance() == acceptFee + relayFee + curatorFee), + implies(!state.closed, balance() == 0), "balance accurate before close" ) - // REM missing invariant balance accurate after close + .invariant( + implies( + state.closed, + balance() == state.distr.slice(2, DIST_LENGTH - 2).sum() + ), + "balance accurate after close" + ) .while(!state.closed) .paySpec([activeToken]) // api: updates current price + // allows anybody to update price .api_(a.touch, () => { - check(state.currentPrice >= floorPrice); + check( + state.currentPrice >= floorPrice, + "currentPrice must be greater than or equal to floorPrice" + ); return [ (k) => { k(null); @@ -324,13 +309,25 @@ export const App = (map) => { dk ), }, + mctc, ]; }, ]; }) // api: accepts offer + // allows anybody but curator to accept offer + // transfers 1% to addr + // calculates proceeding take + // transfers reamining to seller + // transfers accept fee, diff, and token amount to buy + // transfers currator fee to curator .api_(a.acceptOffer, (cAddr) => { check(cAddr != this, "cannot accept offer as curator"); + check( + state.addrs[ADDR_RESERVED_ACTIVE_BIDDER] != this, + "cannot accept offer as bidder" + ); + check(Manager != this, "cannot accept offer as manager"); return [ [state.currentPrice, [0, activeToken]], (k) => { @@ -343,110 +340,179 @@ export const App = (map) => { ); // expect state[cp] >= bal const diff = state.currentPrice - bal; - const cent = bal / 100; - const remaining = bal - cent; - const partTake = remaining / royaltyCap; + const partTake = bal / royaltyCap; const proceedTake = partTake * distrTake; - const sellerTake = remaining - proceedTake; - transfer([cent, [state.activeAmount, activeToken]]).to(addr); - transfer(sellerTake).to(Auctioneer); - transfer([acceptFee + diff, [tokenAmount, token]]).to(this); - transfer(curatorFee).to(cAddr); + const sellerTake = bal - proceedTake; + transfer(distr[0] * partTake).to( + state.addrs[ADDR_RESERVED_ACTIVE_BIDDER] + ); + transfer(distr[1] * partTake).to(state.addrs[ADDR_RESERVED_CURATOR]); + transfer(sellerTake).to(Manager); + transfer([diff, [tokenAmount, token]]).to(this); + switch (mctc) { + case Some: + rUnstake(mctc); + case None: + } return [ { ...state, + addrs: Array.set(state.addrs, ADDR_RESERVED_CURATOR, cAddr), + distr: Array.set( + Array.set( + distr.map((d) => d * partTake), + ADDR_RESERVED_ACTIVE_BIDDER, + 0 + ), + ADDR_RESERVED_CURATOR, + 0 + ), currentPrice: bal, who: this, closed: true, - curatorAddr: cAddr, - partTake, }, + mctc, ]; }, ]; }) - // api: bid - .api_(a.bid, (msg) => { - check(msg > state.activeAmount, "bid must be greater than active amount"); + // api: cancel + // allows auctioneer to cancel auction + // transfers accept and curator fee and token(s) back to auctionee + // unstakes active token if any + .api_(a.cancel, () => { + check(this == Manager, "only auctioneer can cancel"); return [ - [bidFee, [msg, activeToken]], (k) => { k(null); - transfer(bidFee).to(addr); - transfer(state.activeAmount, activeToken).to(state.activeAddr); + transfer([[tokenAmount, token]]).to(this); + switch (mctc) { + case Some: + rUnstake(mctc); + case None: + } return [ { ...state, - activeAmount: msg, - activeAddr: this, + distr: Array.replicate(DIST_LENGTH, 0), + closed: true, + currentPrice: 0, + activeAmount: 0, + activeCtc: getContract(), }, + MContract.None(), ]; }, ]; }) - // api: claim - .api_(a.unlock, () => { - check(this == addr, "only master can unlock"); + // api: bid + // allows anybody to supersede the current bid + // transfer bid fee in network token and non-network token (active token) to addr + // unlock active token if any + .api_(a.bid, (ctc) => { return [ + [0, [FEE_MIN_ACTIVE_BID, activeToken]], (k) => { k(null); + transfer([0, [FEE_MIN_ACTIVE_BID, activeToken]]).to(addr); + const { manager: r1Manager, tokenAmount: r1TokenAmount } = rStake( + ctc, + activeToken, + state.activeAmount + ); + switch (mctc) { + case Some: + rUnstake(mctc); + case None: + } return [ { ...state, - activeAddr: this + currentPrice: priceFunc(thisConsensusSecs())( + startPrice, + floorPrice, + referenceConcensusSecs, + dk + ), + addrs: Array.set( + state.addrs, + ADDR_RESERVED_ACTIVE_BIDDER, + r1Manager + ), + activeAmount: r1TokenAmount, + activeCtc: ctc, }, + MContract.Some(ctc), ]; - } + }, ]; }) - // api: cancels auction - .api_(a.cancel, () => { - check(this == Auctioneer, "only auctioneer can cancel"); + // api: bid cancel + // allows the bidder to cancel their bid + // unstakes active token if any + .api_(a.bidCancel, () => { + check( + this == state.addrs[ADDR_RESERVED_ACTIVE_BIDDER], + "only active bidder can cancel bid" + ); return [ (k) => { k(null); - transfer([acceptFee + curatorFee, [tokenAmount, token]]).to(this); - transfer(state.activeAmount, activeToken).to(state.activeAddr); + switch (mctc) { + case Some: + rUnstake(mctc); + case None: + } return [ { ...state, - closed: true, + currentPrice: priceFunc(thisConsensusSecs())( + startPrice, + floorPrice, + referenceConcensusSecs, + dk + ), + addrs: Array.set( + state.addrs, + ADDR_RESERVED_ACTIVE_BIDDER, + Manager + ), + activeAmount: 0, + activeCtc: getContract(), }, + MContract.None(), ]; }, ]; }) .timeout(false); + e.appClose(); commit(); - Relay.publish(); // Step - ((recvAmount, pDistr) => { - transfer(pDistr[0]).to(state.activeAddr); // reserved - transfer(pDistr[1]).to(addrs[1]); - transfer(pDistr[2]).to(addrs[2]); - transfer(pDistr[3]).to(addrs[3]); - commit(); - - // Step - Relay.publish(); - transfer(pDistr[4]).to(addrs[4]); - transfer(pDistr[5]).to(addrs[5]); - transfer(pDistr[6]).to(addrs[6]); - transfer(pDistr[7]).to(addrs[7]); - commit(); - - Relay.only(() => { - const rAddr = this; - }); - // Step - Relay.publish(rAddr); - transfer(recvAmount).to(rAddr); + Relay.publish(); + if ( + state.who == Manager || + state.distr.slice(2, DIST_LENGTH - 2).sum() == 0 + ) { + transfer(state.distr.slice(2, DIST_LENGTH - 2).sum()).to(Manager); commit(); exit(); - })( - balance() - distrTake * state.partTake, - distr.map((d) => d * state.partTake) - ); + } + transfer(state.distr[2]).to(addrs[2]); + transfer(state.distr[3]).to(addrs[3]); + transfer(state.distr[4]).to(addrs[4]); + transfer(state.distr[5]).to(addrs[5]); + commit(); + // Step + Anybody.publish(); + if (state.distr.slice(6, DIST_LENGTH - 6).sum() != 0) { + transfer(state.distr[6]).to(addrs[6]); + transfer(state.distr[7]).to(addrs[7]); + transfer(state.distr[8]).to(addrs[8]); + transfer(state.distr[9]).to(addrs[9]); + } + commit(); + exit(); }; // -----------------------------------------------