z_govdao_only_principal_filetest.gno
4.51 Kb · 95 lines
1// PKGPATH: gno.land/r/zzprincipal
2//
3// The repointed authority names GovDAO, so a foreign realm crossing into
4// valopers is NOT the principal the gate accepts.
5//
6// This is the runtime counterpart to
7// ./z_foreign_realm_capability_filetest.gno. That file pins that the
8// privileged CAPABILITY cannot be obtained; this one pins that the
9// PRINCIPAL cannot be minted — which is the other half of what the gate
10// depends on, and the half that was actually broken.
11//
12// An earlier version of this file asserted only the rendered description
13// plus "instructions were not defaced" without ever attempting a write, so
14// it went red on a repoint revert purely because the golden path string
15// changed, and stayed GREEN with the authority swapped for a fully
16// permissive one. It now drives the two routes an attacker actually has.
17//
18// Why it matters: under the previous ContractAuthority(ownPath) +
19// DoByCurrent pairing this property did not hold at all. rlm.Address()
20// inside any valopers crossing frame is unconditionally valopers' own
21// address, so the gate accepted every caller and its only real defence was
22// that no exported entrypoint happened to reach updateInstructions.
23//
24// And pointing the gate at r/gov/dao only helps while that path is
25// unmintable. dao.NewSimpleExecutor + SimpleExecutor.Execute are both
26// exported, and Execute is declared in r/gov/dao, so before Execute gated
27// on its invoker ANY realm could mint a frame whose Previous() was
28// r/gov/dao and satisfy this gate — directly for a closure it obtained,
29// and for any exported valopers function whose signature was assignable to
30// `func(realm) error`. Section 2 below is the pin for that.
31package zzprincipal
32
33import (
34 "strings"
35
36 "gno.land/p/nt/testutils/v0"
37 "gno.land/r/gnops/valopers"
38 "gno.land/r/gov/dao"
39 daov3init "gno.land/r/gov/dao/init/v0"
40)
41
42// A real GovDAO exists and this realm is not a member of it, so the final
43// step fails on the proposal-creation gate rather than on "DAO not
44// initialized" -- which would pass for the wrong reason.
45var govdaoMember address = testutils.TestAddress("govdao-member")
46
47func init(cur realm) {
48 daov3init.InitWithUsers(cross(cur), govdaoMember)
49}
50
51// reachInstructions is the shape an attacker needs: something declared
52// where it will run with a useful frame. Declared HERE, so it carries the
53// attacker's identity, not valopers' — the point being that even with the
54// governance principal minted for it, it cannot reach the write.
55func reachInstructions(cur realm) error {
56 // Nothing exported by valopers reaches updateInstructions, so the most
57 // an attacker can do from a forged governance frame is build the sealed
58 // request again. Build it, to prove construction is not the hole.
59 _ = valopers.NewInstructionsProposalRequest(cross(cur), "PWNED-VIA-FORGED-PRINCIPAL")
60 return nil
61}
62
63func main(cur realm) {
64 // 1. The authority names GovDAO, and says so in a way a reader can act
65 // on. A non-canonical proposer cannot reproduce these bytes.
66 println("authority:", valopers.Auth())
67
68 // 2. The governance principal is not mintable. Wrapping any callback in
69 // dao's own exported executor used to hand out a frame whose
70 // Previous() is r/gov/dao; Execute now refuses an invoker from
71 // outside the proxy.
72 e := dao.NewSimpleExecutor(0, cur, reachInstructions, "")
73 println("forged-principal Execute:", e.Execute(cross(cur)))
74
75 // 3. The only remaining route is to ask GovDAO to adopt a request. A
76 // hostile realm cannot: this aborts (a GovDAO member still can, and
77 // then needs the votes — covered by
78 // r/gnops/valopers/proposal/filetests/z_governed_instructions_filetest.gno).
79 rendered := valopers.Render("")
80 println("instructions defaced:", strings.Contains(rendered, "PWNED"))
81 println("authority unchanged:", valopers.Auth())
82
83 dao.MustCreateProposal(cross(cur),
84 valopers.NewAuthorityRotationProposalRequest(cross(cur), "gno.land/r/attacker/dao"))
85 println("UNREACHABLE: outsider rotated the authority")
86}
87
88// Output:
89// authority: contract_authority[contract=gno.land/r/gov/dao,proposer=contract-identity]
90// forged-principal Execute: execution denied: executors are only invocable by gno.land/r/gov/dao
91// instructions defaced: false
92// authority unchanged: contract_authority[contract=gno.land/r/gov/dao,proposer=contract-identity]
93
94// Error:
95// proposal creation must be done directly by a user or through the r/gov/dao proxy. caller realm: realm{gno.land/r/gov/dao:g1p84dvfh4wrplyxx4zsmy77a8rxcnjup2j7zv5r}; caller's previous: realm{gno.land/r/zzprincipal:g1036vewy55meppdzpngzsufahr389mldycavect}