Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

escrow_host.gno

6.81 Kb · 199 lines
  1package memba_weighted_host
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"gno.land/p/samcrew/memba_weighted_policy"
  7)
  8
  9type EscrowMilestone struct {
 10	ID                                int
 11	Amount                            int64
 12	Status                            string
 13	FundedAt, CompletedAt, DisputedAt int64
 14	PreDisputeStatus                  string
 15}
 16type EscrowContract struct {
 17	Exists                  bool
 18	ID, ContentHash, Status string
 19	Client, Freelancer      address
 20	Count                   int
 21	Milestones              [20]EscrowMilestone
 22}
 23type EscrowFeeTerms struct {
 24	RawBPS, EffectiveBPS                             int64
 25	RawTreasury, FallbackTreasury, EffectiveTreasury address
 26}
 27// EscrowState is the escrow_v4 state a proposal freezes. The pause fields copy
 28// PauseInfo except PausedBlocks, which grows every block. ExitsOpen is also
 29// height-derived: it is read fresh at proposal and execution time and never
 30// enters the commitment (escrowStateJSON), so a pause ageing past its blocking
 31// window cannot make a frozen proposal stale.
 32type EscrowState struct {
 33	Owner, PendingOwner               address
 34	FeeRecipient, PendingFeeRecipient address
 35	Paused                            bool
 36	PausedAt, ExitsReopenAt           int64
 37	CooldownUntil                     int64
 38	ExitsOpen                         bool
 39	Contract                          EscrowContract
 40	Fees                              EscrowFeeTerms
 41}
 42type escrowProposal struct {
 43	action EscrowAction
 44	before EscrowState
 45}
 46
 47func escrowCommitment(a EscrowAction, s EscrowState) string {
 48	return a.Bytes() + "|before|" + escrowStateJSON(s)
 49}
 50func (h *Host) validateEscrowAuthority(s EscrowState) {
 51	if h.escrowPolicy == nil {
 52		panic("escrow actions are not configured")
 53	}
 54	if !s.Owner.IsValid() || (s.PendingOwner != "" && !s.PendingOwner.IsValid()) || !s.FeeRecipient.IsValid() || (s.PendingFeeRecipient != "" && !s.PendingFeeRecipient.IsValid()) {
 55		panic("invalid escrow authority pre-state")
 56	}
 57	if s.CooldownUntil < 0 || (!s.Paused && (s.PausedAt != 0 || s.ExitsReopenAt != 0 || !s.ExitsOpen)) || (s.Paused && (s.PausedAt <= 0 || s.ExitsReopenAt <= s.PausedAt)) {
 58		panic("invalid escrow pause pre-state")
 59	}
 60}
 61func (h *Host) validateEscrow(a EscrowAction, s EscrowState) {
 62	h.validateEscrowAuthority(s)
 63	a.Bytes()
 64	p := h.escrowPolicy
 65	if !a.IsResolution() && (s.Contract != (EscrowContract{}) || s.Fees != (EscrowFeeTerms{})) {
 66		panic("unexpected escrow dispute pre-state")
 67	}
 68	if a.Kind() == EscrowAcceptOwner {
 69		if s.Owner == p.DAO() || s.PendingOwner != p.DAO() {
 70			panic("DAO is not pending escrow owner")
 71		}
 72		return
 73	}
 74	if s.Owner != p.DAO() {
 75		panic("DAO is not escrow owner")
 76	}
 77	if a.Kind() == EscrowAbortReturn {
 78		if a.Recipient() != p.Successor() || s.PendingOwner != p.Successor() {
 79			panic("escrow return does not match policy")
 80		}
 81		return
 82	}
 83	if s.PendingOwner != "" {
 84		panic("escrow authority transfer is pending")
 85	}
 86	switch a.Kind() {
 87	case EscrowReturnOwner:
 88		if a.Recipient() != p.Successor() {
 89			panic("escrow successor does not match policy")
 90		}
 91	case EscrowUnpause:
 92		if !s.Paused {
 93			panic("escrow unpause is a no-op")
 94		}
 95	case EscrowSetFeeRecipient:
 96		r := a.Recipient()
 97		if r == p.DAO() || r == chain.PackageAddress(EscrowPath) {
 98			panic("escrow fee recipient cannot be governance or escrow")
 99		}
100		if r == s.FeeRecipient || r == s.PendingFeeRecipient {
101			panic("escrow fee recipient change is a no-op")
102		}
103	case EscrowRefundClient, EscrowPayFreelancer:
104		// escrow_v4 refuses ResolveDispute inside a pause's blocking window;
105		// refuse the proposal then too, at creation and again at execution.
106		if s.Paused && !s.ExitsOpen {
107			panic("escrow exits are paused")
108		}
109		c := s.Contract
110		if !c.Exists || c.ID != a.ContractID() || c.Count < 1 || c.Count > 20 || a.Milestone() >= c.Count || len(c.ContentHash) != 64 || !c.Client.IsValid() {
111			panic("invalid escrow contract pre-state")
112		}
113		for i, m := range c.Milestones {
114			if i >= c.Count {
115				if m != (EscrowMilestone{}) {
116					panic("unexpected escrow milestone pre-state")
117				}
118				continue
119			}
120			if m.Amount <= 0 {
121				panic("invalid escrow milestone amount")
122			}
123		}
124		m := c.Milestones[a.Milestone()]
125		if c.Status != "disputed" || m.Status != "disputed" || (m.PreDisputeStatus != "funded" && m.PreDisputeStatus != "completed") {
126			panic("escrow milestone is not disputed")
127		}
128		if a.Kind() == EscrowRefundClient {
129			if c.Client == p.DAO() || c.Client == chain.PackageAddress(EscrowPath) {
130				panic("escrow payout cannot be trapped in governance or escrow")
131			}
132			if s.Fees != (EscrowFeeTerms{}) {
133				panic("unexpected escrow refund fee terms")
134			}
135			return
136		}
137		if !c.Freelancer.IsValid() || s.Fees.EffectiveBPS < 0 || s.Fees.EffectiveBPS > 500 {
138			panic("invalid escrow payout terms")
139		}
140		if c.Freelancer == p.DAO() || c.Freelancer == chain.PackageAddress(EscrowPath) {
141			panic("escrow payout cannot be trapped in governance or escrow")
142		}
143		fee := (m.Amount/10000)*s.Fees.EffectiveBPS + ((m.Amount%10000)*s.Fees.EffectiveBPS)/10000
144		if fee > 0 && (s.Fees.EffectiveTreasury == p.DAO() || s.Fees.EffectiveTreasury == chain.PackageAddress(EscrowPath)) {
145			panic("escrow payout cannot be trapped in governance or escrow")
146		}
147		if fee > 0 && !s.Fees.EffectiveTreasury.IsValid() {
148			panic("invalid escrow fee recipient")
149		}
150	default:
151		panic("unsupported escrow action")
152	}
153}
154func (h *Host) ProposeEscrow(a EscrowAction, s EscrowState, rlm realm) uint64 {
155	who := assertCaller(h, rlm)
156	h.validateEscrow(a, s)
157	p := &roleProposal{proposer: who, escrow: &escrowProposal{action: a, before: s}}
158	p.ballot = h.policy.NewProposal(a.Category(), escrowCommitment(a, s))
159	h.addProposal(p)
160	return uint64(len(h.proposals))
161}
162func (h *Host) invalidateEscrowPeers(cause string, id uint64) {
163	members := []memba_weighted_policy.Member{}
164	for _, s := range h.seats {
165		members = append(members, s.Member)
166	}
167	h.policy.Reconfigure(members)
168	h.recordInvalidation(cause, id, EscrowPath)
169}
170func (h *Host) ConsumeEscrow(id uint64, s EscrowState, rlm realm) EscrowAction {
171	assertCaller(h, rlm)
172	p := h.proposal(id)
173	if p.escrow == nil {
174		panic("proposal is not an escrow action")
175	}
176	h.validateEscrow(p.escrow.action, s)
177	p.ballot.Consume(escrowCommitment(p.escrow.action, s))
178	h.invalidateEscrowPeers(InvalidatedByExecution, id)
179	return p.escrow.action
180}
181func (h *Host) PrepareEscrowEmergencyPause(s EscrowState, rlm realm) {
182	assertCaller(h, rlm)
183	h.validateEscrowAuthority(s)
184	if s.Owner != h.escrowPolicy.DAO() {
185		panic("DAO is not escrow owner")
186	}
187	if s.Paused {
188		panic("escrow is already paused")
189	}
190	// escrow_v4 refuses a Pause before its cooldown ends. Refuse here first,
191	// before invalidating any proposal.
192	if runtime.ChainHeight() < s.CooldownUntil {
193		panic("escrow pause cooldown")
194	}
195	if s.Contract != (EscrowContract{}) || s.Fees != (EscrowFeeTerms{}) {
196		panic("unexpected escrow dispute pre-state")
197	}
198	h.invalidateEscrowPeers(InvalidatedByPause, 0)
199}