Skip to content

Commit 7b10491

Browse files
committed
test: Revise on-chain governance architectures to allow more flexible Yearn Governance designs. Revise README
1 parent 3bee793 commit 7b10491

File tree

2 files changed

+138
-51
lines changed

2 files changed

+138
-51
lines changed

README-yearnBorg.md

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,22 +79,77 @@ all coming operations listed above will require approval of both `ychad.eth` and
7979

8080
### Future On-chain Governance Transition
8181

82-
Yearn's Snapshot governance will be replaced with an on-chain governance at some point (ex. `YearnGovExecutor`).
83-
`YearnGovExecutor` (or its adapter) must satisfy the following requirements to integrate with the co-approval process:
84-
- Each proposal must include generic transaction fields (`target`, `value`, `calldata` or their equivalents) to enable `YearnGovExecutor` to execute the proposal upon approval
85-
- Proposals involving `ychad.eth` [Restricted Admin Operations](#restricted-admin-operations) must be executed solely by `ychad.eth` to enforce co-approval requirements
82+
Yearn's Snapshot-based governance will transition to an on-chain governance system (ex. `YearnGovernance`).
83+
An adapter (`YearnGovernanceAdapter`) will be implemented by MetaLex to manage the implementation details on co-approval process.
84+
To integrate successfully, `YearnGovernance` must meet the following requirements:
85+
86+
- Each proposal has an unique ID (ex. `proposalId`)
87+
- `YearnGovernanceAdapter` can read the proposal's voting result and verify it is passed
88+
- `YearnGovernanceAdapter` can extract the admin operation (ex. `target`, `value`, `calldata` or equivalent) from the proposal
8689

8790
The transition process from Snapshot to on-chain governance is listed as follows:
8891

89-
1. A final Snapshot proposal will be submitted to replace `Snapshot Executor` with `YearnGovExecutor` by transferring ownership of `SudoImplant` and `EjectImplant` to `YearnGovExecutor`
90-
2. Once co-approved and executed by `ychad.eth`, the transition process is complete
92+
1. A final Snapshot proposal will be submitted to grant `YearnGovernanceAdapter` ownership of the implants
93+
2. `ychad.eth` to co-approved and executed the proposal
94+
3. The first on-chain proposal will be submitted to revoke `SnapShotExecutor` ownership of the implants
95+
4. `ychad.eth` to co-approved and executed the proposal. The transition is now complete
9196

9297
After the transition, the co-approval process will become:
9398

9499
1. Operation is initiated on the MetaLeX OS webapp
95100
2. An on-chain proposal will be submitted to `YearnGovExecutor`
96101
3. Once the vote passed, `ychad.eth` will co-approve it by executing the operation through the MetaLeX OS webapp
97102

103+
Below shows the changes of BORG architectures before/after on-chain governance transition:
104+
105+
```mermaid
106+
graph TD
107+
ychad[ychad.eth<br/>6/9 signers]
108+
109+
subgraph offChainGovernance["Snapshot Governance (before)"]
110+
yearnDaoVoting[Yearn DAO Voting Snapshot]
111+
oracleAddr[oracle]
112+
snapshotExecutor[Snapshot Executor]
113+
end
114+
115+
subgraph onChainGovernance["On-chain Governance (after)"]
116+
yearnGovernance[Yearn Governance]
117+
yearnGovernanceAdapter[Yearn Governance Adapter]
118+
end
119+
120+
subgraph implants["Implants (Modules)"]
121+
ejectImplant{{Eject Implant}}
122+
sudoImplant{{Sudo Implant}}
123+
end
124+
125+
ychad -->|"owner<br>execute(proposalId)"| snapshotExecutor
126+
127+
oracleAddr -->|"oracle<br>propose(admin operation)"| snapshotExecutor
128+
oracleAddr -->|monitor| yearnDaoVoting
129+
130+
snapshotExecutor -->|"owner<br>admin operation()"| implants
131+
132+
ychad -->|"owner<br>execute(proposalId)"| yearnGovernanceAdapter
133+
134+
yearnGovernanceAdapter -->|"verify voting results of proposalId<br>and extract the admin operation"| yearnGovernance
135+
yearnGovernanceAdapter -->|"owner<br>admin operation()"| implants
136+
137+
%% Styling (optional, Mermaid supports limited styling)
138+
classDef default fill:#191918,stroke:#fff,stroke-width:2px,color:#fff;
139+
classDef borg fill:#191918,stroke:#E1FE52,stroke-width:2px,color:#E1FE52;
140+
classDef yearn fill:#191918,stroke:#2C68DB,stroke-width:2px,color:#2C68DB;
141+
classDef safe fill:#191918,stroke:#76FB8D,stroke-width:2px,color:#76FB8D;
142+
classDef todo fill:#191918,stroke:#F09B4A,stroke-width:2px,color:#F09B4A;
143+
class ejectImplant borg;
144+
class sudoImplant borg;
145+
class snapshotExecutor borg;
146+
class oracleAddr borg;
147+
class yearnGovernanceAdapter borg;
148+
class ychad yearn;
149+
class yearnDaoVoting yearn;
150+
class yearnGovernance yearn;
151+
```
152+
98153
### Module Addition
99154

100155
New Modules grant `ychad.eth` privileges to bypass Guards restrictions, therefore it requires DAO co-approval via [Co-approval Workflows](#co-approval-workflows).

test/yearnBorgAcceptance.t.sol

Lines changed: 77 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,67 @@ pragma solidity 0.8.20;
33

44
import "forge-std/Test.sol";
55
import "solady/tokens/ERC20.sol";
6-
import {Ownable} from "openzeppelin/contracts/access/Ownable.sol";
76
import {borgCore} from "../src/borgCore.sol";
87
import {ejectImplant} from "../src/implants/ejectImplant.sol";
98
import {sudoImplant} from "../src/implants/sudoImplant.sol";
10-
import {BorgAuth} from "../src/libs/auth.sol";
9+
import {BorgAuth, BorgAuthACL} from "../src/libs/auth.sol";
1110
import {SnapShotExecutor} from "../src/libs/governance/snapShotExecutor.sol";
1211
import {SafeTxHelper} from "./libraries/safeTxHelper.sol";
1312
import {IGnosisSafe, GnosisTransaction, IMultiSendCallOnly} from "../test/libraries/safe.t.sol";
1413

15-
contract YearnGovExecutor is Ownable {
16-
struct proposal {
14+
contract MockYearnGovernance {
15+
struct Proposal {
1716
address target;
1817
uint256 value;
1918
bytes cdata;
2019
string description;
2120
}
2221

23-
mapping(bytes32 => proposal) public pendingProposals;
24-
25-
constructor(address owner) Ownable(owner) {}
22+
mapping(bytes32 => Proposal) public proposals;
23+
24+
// Assume this is how to get a proposal's content (including admin operation's data)
25+
function getProposal(bytes32 proposalId) external returns (Proposal memory) {
26+
return proposals[proposalId];
27+
}
28+
29+
// Assume this is how to verify a proposal is passed
30+
function isProposalPassed(bytes32 proposalId) external returns (bool) {
31+
return true;
32+
}
2633

27-
// Propose for voting
28-
function propose(address target, uint256 value, bytes calldata cdata, string memory description) external returns (bytes32) {
29-
bytes32 proposalId = keccak256(abi.encodePacked(target, value, cdata, description));
30-
pendingProposals[proposalId] = proposal(target, value, cdata, description);
34+
// Assume this is how to propose an admin operation
35+
function propose(Proposal calldata p) external returns (bytes32) {
36+
bytes32 proposalId = keccak256(abi.encodePacked(p.target, p.value, p.cdata, p.description));
37+
proposals[proposalId] = p;
3138
return proposalId;
3239
}
40+
}
41+
42+
contract MockYearnGovernanceAdapter is BorgAuthACL {
43+
error YearnGovernanceAdapter_ProposalNotPassed(bytes32 proposalId);
44+
error YearnGovernanceAdapter_ProposalAlreadyExecuted(bytes32 proposalId);
45+
46+
MockYearnGovernance yearnGovernance;
47+
mapping(bytes32 => bool) public proposalExecuted;
3348

34-
// Execute passed proposal (for testing we assume it always passes)
49+
constructor(BorgAuth _auth, MockYearnGovernance _yearnGovernance) BorgAuthACL(_auth) {
50+
yearnGovernance = _yearnGovernance;
51+
}
52+
53+
// Only owner (ychad.eth) is allowed to execute the admin operation. This is part of the co-approval process.
3554
function execute(bytes32 proposalId) payable external onlyOwner() {
36-
proposal memory p = pendingProposals[proposalId];
55+
if (!yearnGovernance.isProposalPassed(proposalId)) {
56+
revert YearnGovernanceAdapter_ProposalNotPassed(proposalId);
57+
}
58+
59+
if (proposalExecuted[proposalId]) {
60+
revert YearnGovernanceAdapter_ProposalAlreadyExecuted(proposalId);
61+
}
62+
63+
MockYearnGovernance.Proposal memory p = yearnGovernance.getProposal(proposalId);
64+
proposalExecuted[proposalId] = true;
65+
3766
(bool success, ) = p.target.call{value: p.value}(p.cdata);
38-
delete pendingProposals[proposalId];
3967
}
4068
}
4169

@@ -269,28 +297,32 @@ contract YearnBorgAcceptanceTest is Test {
269297

270298
/// @dev Transition to on-chain governance should be successful with co-approval
271299
function testOnChainGovernanceTransition() public {
272-
YearnGovExecutor yearnGovExecutor = new YearnGovExecutor(address(ychadSafe));
300+
// Yearn to deploy on-chain governance contract
301+
MockYearnGovernance yearnGovernance = new MockYearnGovernance();
302+
303+
// MetaLeX to deploy adapter
304+
MockYearnGovernanceAdapter yearnGovernanceAdapter = new MockYearnGovernanceAdapter(snapShotExecutor.AUTH(), yearnGovernance);
273305

274306
BorgAuth implantAuth = eject.AUTH();
275307
uint256 ownerRole = implantAuth.OWNER_ROLE();
276308

277309
// Should not be owner yet
278-
vm.expectRevert(abi.encodeWithSelector(BorgAuth.BorgAuth_NotAuthorized.selector, ownerRole, address(yearnGovExecutor)));
279-
implantAuth.onlyRole(ownerRole, address(yearnGovExecutor));
310+
vm.expectRevert(abi.encodeWithSelector(BorgAuth.BorgAuth_NotAuthorized.selector, ownerRole, address(yearnGovernanceAdapter)));
311+
implantAuth.onlyRole(ownerRole, address(yearnGovernanceAdapter));
280312

281313
// Simulate on-chain governance transition
282314
{
283-
// SnapShotExecutor to add YearnGovExecutor as owner
315+
// SnapShotExecutor to add yearnGovernanceAdapter as owner
284316
vm.prank(oracle);
285317
bytes32 proposalIdAddOwner = snapShotExecutor.propose(
286318
address(implantAuth), // target
287319
0, // value
288320
abi.encodeWithSelector(
289321
implantAuth.updateRole.selector,
290-
address(yearnGovExecutor),
322+
address(yearnGovernanceAdapter),
291323
ownerRole
292324
), // cdata
293-
"Add yearnGovExecutor as owner"
325+
"Add yearnGovernanceAdapter as owner"
294326
);
295327

296328
// After waiting period
@@ -306,28 +338,28 @@ contract YearnBorgAcceptanceTest is Test {
306338
)
307339
}));
308340

309-
// YearnGovExecutor should be an owner now
310-
implantAuth.onlyRole(ownerRole, address(yearnGovExecutor));
341+
// yearnGovernanceAdapter should be an owner now
342+
implantAuth.onlyRole(ownerRole, address(yearnGovernanceAdapter));
311343

312-
// YearnGovExecutor to remove SnapShotExecutor's ownership
313-
bytes32 proposalIdRemoveOwner = yearnGovExecutor.propose(
314-
address(implantAuth), // target
315-
0, // value
316-
abi.encodeWithSelector(
344+
// YearnGovernance to revoke SnapShotExecutor ownership
345+
bytes32 proposalIdRevokeOwner = yearnGovernance.propose(MockYearnGovernance.Proposal({
346+
target: address(implantAuth),
347+
value: 0,
348+
cdata: abi.encodeWithSelector(
317349
implantAuth.updateRole.selector,
318350
address(snapShotExecutor),
319351
0
320-
), // cdata
321-
"Remove snapShotExecutor ownership"
322-
);
352+
),
353+
description: "Revoke snapShotExecutor ownership"
354+
}));
323355

324-
// Execute the proposal
356+
// Execute the passed proposal
325357
safeTxHelper.executeSingle(GnosisTransaction({
326-
to: address(yearnGovExecutor),
358+
to: address(yearnGovernanceAdapter),
327359
value: 0,
328360
data: abi.encodeWithSelector(
329-
yearnGovExecutor.execute.selector,
330-
proposalIdRemoveOwner
361+
MockYearnGovernanceAdapter.execute.selector,
362+
proposalIdRevokeOwner
331363
)
332364
}));
333365

@@ -341,26 +373,26 @@ contract YearnBorgAcceptanceTest is Test {
341373
vm.assertFalse(ychadSafe.isOwner(alice), "Should not be Safe signer");
342374

343375
// Simulate a proposal (and it is immediately passed)
344-
bytes32 proposalId = yearnGovExecutor.propose(
345-
address(eject), // target
346-
0, // value
347-
abi.encodeWithSelector(
376+
bytes32 proposalId = yearnGovernance.propose(MockYearnGovernance.Proposal({
377+
target: address(eject),
378+
value: 0,
379+
cdata: abi.encodeWithSelector(
348380
bytes4(keccak256("addOwner(address)")),
349381
alice // newOwner
350-
), // cdata
351-
"Add Alice as new signer"
352-
);
382+
),
383+
description: "Add Alice as new signer"
384+
}));
353385

354386
// Should fail if not executed from ychad.eth
355-
vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(this)));
356-
yearnGovExecutor.execute(proposalId);
387+
vm.expectRevert(abi.encodeWithSelector(BorgAuth.BorgAuth_NotAuthorized.selector, ownerRole, address(this)));
388+
yearnGovernanceAdapter.execute(proposalId);
357389

358390
// Should succeed if executed from ychad.eth
359391
safeTxHelper.executeSingle(GnosisTransaction({
360-
to: address(yearnGovExecutor),
392+
to: address(yearnGovernanceAdapter),
361393
value: 0,
362394
data: abi.encodeWithSelector(
363-
yearnGovExecutor.execute.selector,
395+
MockYearnGovernanceAdapter.execute.selector,
364396
proposalId
365397
)
366398
}));

0 commit comments

Comments
 (0)