escrow_adapter.gno
7.08 Kb · 78 lines
1package memba_dao
2import("chain";"chain/banker";"chain/runtime";"gno.land/p/samcrew/memba_weighted_host";"gno.land/r/samcrew/escrow_v4")
3func escrowState(a memba_weighted_host.EscrowAction)memba_weighted_host.EscrowState{
4 p:=escrow_v4.PauseState();if p.Paused!=escrow_v4.IsPaused(){panic("invalid escrow pause read")}
5 s:=memba_weighted_host.EscrowState{Owner:address(escrow_v4.GetAdmin()),PendingOwner:address(escrow_v4.GetPendingAdmin()),FeeRecipient:address(escrow_v4.GetFeeRecipient()),PendingFeeRecipient:address(escrow_v4.GetPendingFeeRecipient()),Paused:p.Paused,PausedAt:p.PausedAt,ExitsReopenAt:p.ExitsReopenAt,CooldownUntil:p.CooldownUntil,ExitsOpen:p.ExitsOpen}
6 if a.IsResolution(){
7 c:=escrow_v4.GetGovernanceContract(a.ContractID());s.Contract=memba_weighted_host.EscrowContract{Exists:c.Exists,ID:c.ID,ContentHash:c.ContentHash,Status:c.Status,Client:c.Client,Freelancer:c.Freelancer,Count:c.Count}
8 for i,m:=range c.Milestones{s.Contract.Milestones[i]=memba_weighted_host.EscrowMilestone{ID:m.ID,Amount:m.Amount,Status:m.Status,FundedAt:m.FundedAt,CompletedAt:m.CompletedAt,DisputedAt:m.DisputedAt,PreDisputeStatus:m.PreDisputeStatus}}
9 }
10 if a.Kind()==memba_weighted_host.EscrowPayFreelancer{f:=escrow_v4.GetGovernanceFeeTerms();s.Fees=memba_weighted_host.EscrowFeeTerms{RawBPS:f.RawBPS,EffectiveBPS:f.EffectiveBPS,RawTreasury:f.RawTreasury,FallbackTreasury:f.FallbackTreasury,EffectiveTreasury:f.EffectiveTreasury}}
11 return s
12}
13func proposeEscrow(cur realm,a memba_weighted_host.EscrowAction)uint64{return host.ProposeEscrow(a,escrowState(a),cur)}
14func ProposeEscrowAccept(cur realm)uint64{return proposeEscrow(cur,escrowPolicy.Accept())}
15func ProposeEscrowReturn(cur realm)uint64{return proposeEscrow(cur,escrowPolicy.Return())}
16func ProposeEscrowAbortReturn(cur realm)uint64{return proposeEscrow(cur,escrowPolicy.AbortReturn())}
17func ProposeEscrowUnpause(cur realm)uint64{return proposeEscrow(cur,memba_weighted_host.EscrowResume())}
18func ProposeEscrowRefund(cur realm,id string,index int)uint64{return proposeEscrow(cur,memba_weighted_host.EscrowResolution(id,index,true))}
19func ProposeEscrowPayFreelancer(cur realm,id string,index int)uint64{return proposeEscrow(cur,memba_weighted_host.EscrowResolution(id,index,false))}
20// ProposeEscrowFeeRecipient stages recipient as the fallback fee recipient;
21// recipient then accepts on escrow_v4 with its own AcceptFeeRecipient call.
22func ProposeEscrowFeeRecipient(cur realm,recipient address)uint64{return proposeEscrow(cur,memba_weighted_host.EscrowFeeRecipient(recipient))}
23func EmergencyPauseEscrow(cur realm){a:=memba_weighted_host.EscrowResume();before:=escrowState(a);host.PrepareEscrowEmergencyPause(before,cur);h:=runtime.ChainHeight();expected:=before;expected.Paused=true;expected.PausedAt=h;expected.ExitsReopenAt=h+escrow_v4.MaxPauseBlks;expected.ExitsOpen=false;escrow_v4.Pause(cross(cur));if escrowState(a)!=expected{panic("escrow pause postcondition mismatch")}}
24
25type escrowBalance struct{who address;before,delta,want int64}
26func escrowCredit(rows []escrowBalance,who address,amount int64)[]escrowBalance{
27 if amount==0{return rows};if amount<0||!who.IsValid(){panic("invalid escrow credit")}
28 if who==escrowPolicy.DAO()||who==chain.PackageAddress(memba_weighted_host.EscrowPath){panic("escrow payout cannot be trapped in governance or escrow")}
29 for i:=range rows{if rows[i].who==who{rows[i].delta+=amount;return rows}}
30 return append(rows,escrowBalance{who:who,delta:amount})
31}
32// Global solvency is checked at execution, not frozen at proposal creation:
33// unrelated funding or dust may arrive while members vote. Aggregate aliases.
34func escrowSettlement(a memba_weighted_host.EscrowAction,s memba_weighted_host.EscrowState)([]escrowBalance,int64){
35 realmAddr:=chain.PackageAddress(memba_weighted_host.EscrowPath);amount:=s.Contract.Milestones[a.Milestone()].Amount
36 liable:=escrow_v4.TotalLiabilities();bnk:=banker.NewReadonlyBanker()
37 if amount<=0||liable<amount||bnk.GetCoin(realmAddr,"ugnot")<liable{panic("escrow is not solvent")}
38 rows:=[]escrowBalance{{who:realmAddr,delta:-amount}}
39 if a.Kind()==memba_weighted_host.EscrowRefundClient{rows=escrowCredit(rows,s.Contract.Client,amount)}else{
40 bps:=s.Fees.EffectiveBPS;if bps<0||bps>500{panic("invalid escrow payout terms")}
41 fee:=(amount/10000)*bps+((amount%10000)*bps)/10000
42 rows=escrowCredit(rows,s.Contract.Freelancer,amount-fee);rows=escrowCredit(rows,s.Fees.EffectiveTreasury,fee)
43 }
44 for i:=range rows{
45 r:=&rows[i];r.before=bnk.GetCoin(r.who,"ugnot")
46 if r.before<0||(r.delta<0&&r.before< -r.delta)||(r.delta>0&&r.before>9223372036854775807-r.delta){panic("invalid escrow balance delta")}
47 r.want=r.before+r.delta
48 }
49 return rows,liable-amount
50}
51func executeEscrow(cur realm,id uint64){
52 proposed:=host.Proposal(id).EscrowAction;before:=escrowState(proposed);var balances []escrowBalance;var liabilityAfter int64
53 if proposed.IsResolution(){balances,liabilityAfter=escrowSettlement(proposed,before)}
54 a:=host.ConsumeEscrow(id,before,cur);expected:=before
55 switch a.Kind(){
56 case memba_weighted_host.EscrowAcceptOwner:expected.Owner=escrowPolicy.DAO();expected.PendingOwner="";escrow_v4.AcceptOwnership(cross(cur))
57 case memba_weighted_host.EscrowReturnOwner:expected.PendingOwner=a.Recipient();escrow_v4.TransferOwnership(cross(cur),a.Recipient())
58 case memba_weighted_host.EscrowAbortReturn:expected.PendingOwner="";escrow_v4.CancelOwnershipTransfer(cross(cur))
59 case memba_weighted_host.EscrowUnpause:expected.Paused=false;expected.PausedAt=0;expected.ExitsReopenAt=0;expected.ExitsOpen=true;expected.CooldownUntil=runtime.ChainHeight()+escrow_v4.PauseCooldownBlks;escrow_v4.Unpause(cross(cur))
60 case memba_weighted_host.EscrowSetFeeRecipient:expected.PendingFeeRecipient=a.Recipient();escrow_v4.ProposeFeeRecipient(cross(cur),a.Recipient())
61 case memba_weighted_host.EscrowRefundClient,memba_weighted_host.EscrowPayFreelancer:
62 refund:=a.Kind()==memba_weighted_host.EscrowRefundClient
63 expected.Contract.Milestones[a.Milestone()].Status="released";if refund{expected.Contract.Milestones[a.Milestone()].Status="refunded"}
64 // escrow_v4 settles the contract once every milestone is released or
65 // refunded: completed if all were released, cancelled otherwise.
66 expected.Contract.Status="active";allReleased,allTerminal:=true,true
67 for i:=0;i<expected.Contract.Count;i++{m:=expected.Contract.Milestones[i];if m.Status=="disputed"{expected.Contract.Status="disputed"};if m.Status!="released"{allReleased=false};if m.Status!="released"&&m.Status!="refunded"{allTerminal=false}}
68 if allTerminal{expected.Contract.Status="cancelled";if allReleased{expected.Contract.Status="completed"}}
69 escrow_v4.ResolveDispute(cross(cur),a.ContractID(),a.Milestone(),refund)
70 default:panic("unsupported escrow action")
71 }
72 if escrowState(a)!=expected{panic("escrow postcondition mismatch")}
73 if a.IsResolution(){
74 if escrow_v4.TotalLiabilities()!=liabilityAfter{panic("escrow liability postcondition mismatch")}
75 bnk:=banker.NewReadonlyBanker();for _,r:=range balances{if bnk.GetCoin(r.who,"ugnot")!=r.want{panic("escrow balance postcondition mismatch")}}
76 if bnk.GetCoin(chain.PackageAddress(memba_weighted_host.EscrowPath),"ugnot")<liabilityAfter{panic("escrow solvency postcondition mismatch")}
77 }
78}