Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
e2e88db
chore: Add Yearn BORG architectures
Detoo Apr 10, 2025
ee42434
chore: Add member-management workflow
Detoo Apr 10, 2025
3c7503a
chore: Use SnapshotExecutor for member-management workflow
Detoo Apr 11, 2025
f8748ed
chore: Clarify BORG modules ownership and member management
Detoo Apr 14, 2025
a1108fb
chore: Simplify member management voting workflow
Detoo Apr 14, 2025
14f12e8
feat: Pull previous SnapShotExecutor because it fits the requirements…
Detoo Apr 14, 2025
9a2419e
wip: feat: Yearn BORG deploy scripts and tests
Detoo Apr 15, 2025
b277486
wip: feat: Fix typos and add more tests
Detoo Apr 15, 2025
8b23d0e
test: Fork ychad.eth as Safe for tests
Detoo Apr 15, 2025
c199382
feat: Deploy scripts to accommodate external Safe TXs
Detoo Apr 15, 2025
379986f
feat: Simplify deployment process
Detoo Apr 15, 2025
c184272
chore: Update README and remove unused comments
Detoo Apr 15, 2025
8b9af67
feat: Simplify SnapShotExecutor's cancel process. Add tests
Detoo Apr 16, 2025
06a8fb5
chore: Plan for on-chain governance transition
Detoo Apr 16, 2025
c0597f0
chore: Simplify on-chain governance architectures
Detoo Apr 16, 2025
383e4a9
wip: feat: Restrict admin operations
Detoo Apr 17, 2025
43cb2de
feat: Restrict admin operations. Add tests
Detoo Apr 17, 2025
b318b70
feat: Update yearn BORG ownership and admin permissions
Detoo Apr 17, 2025
db99ecc
feat: Implement sudoImplant for DAO/BORG co-approval on admin operations
Detoo Apr 17, 2025
59b2214
chore: Update comments
Detoo Apr 17, 2025
ee6ecaa
test: Add sudoImplant tests
Detoo Apr 18, 2025
23a527b
chore: Add more comments and README
Detoo Apr 18, 2025
898c25d
test: on-chain governance transition
Detoo Apr 18, 2025
4192522
Update README-yearnBorg.md
lex-node Apr 19, 2025
800b9d9
feat: Block SudoImplant from disabling itself to prevent potential us…
Detoo Apr 20, 2025
3bee793
test: Revise on-chain governance transition to prevent potential user…
Detoo Apr 20, 2025
7b10491
test: Revise on-chain governance architectures to allow more flexible…
Detoo Apr 20, 2025
5e01785
test: Revise tests and descriptions
Detoo Apr 21, 2025
34d9a19
feat: snapShotExecutor oracle deadman switch and transfer
Detoo Apr 21, 2025
4157544
feat: ejectImplant to support disallowing self-eject with threshold r…
Detoo Apr 22, 2025
1b0e381
Revert "test: Revise on-chain governance architectures to allow more …
Detoo Apr 28, 2025
62c231f
chore: Update README regarding on-chain governance transition process…
Detoo Apr 28, 2025
37d3829
chore: clean up
Detoo Apr 28, 2025
feecb59
Merge pull request #39 from MetaLex-Tech/lex-node-patch-3
Detoo Apr 28, 2025
2c53941
feat: Allow SnapshotExecutor to transfer oracle through proposal, and…
Detoo Apr 28, 2025
7b6f938
chore: Revise README
Detoo Apr 29, 2025
7d45159
feat: Updatable SnapShotExecutor.oracleTtl in preparation for on-chai…
Detoo Apr 29, 2025
1d3e514
feat: Extend SnapShotExecutor cancellation waiting period for more re…
Detoo Apr 29, 2025
2f3b49f
chore: misc code quality improvements
Detoo Apr 29, 2025
45c0840
feat: Clarify waiting period vs expiry on snapShotExecutor
Detoo May 1, 2025
57c4264
fix: Additional checks to prevent duplicate proposals
Detoo May 17, 2025
97b6d94
chore: Add scripts to replace SnapShotExecutor
Detoo May 17, 2025
00e1628
First pass for blocking delegateCalls
merisman Jun 2, 2025
94653d2
feat: Transfer borgCore ownership to SnapShotExecutor for future poli…
Detoo Jun 2, 2025
9394785
test: BORG policy management through co-approval
Detoo Jun 2, 2025
e2fa8b7
Merge branch 'feat/yearnBorg' into feat/yearnBorg-whitelist-coapproval
Detoo Jun 2, 2025
504ccc5
feat: Whitelist MultiSendCallOnly while blocking all other delegateca…
Detoo Jun 3, 2025
7eb2da0
chore: Add "Restricted Advanced Operations" section to README
Detoo Jun 3, 2025
4393296
Updating to keep logic the same for whitelist, but new for blacklist.
merisman Jun 4, 2025
05925a7
Updating toggle for blacklist mode.
merisman Jun 5, 2025
3132d36
chore: Optimize README sections
Detoo Jun 5, 2025
643f057
Merge remote-tracking branch 'origin/feat/yearnBorg-Blacklist' into f…
Detoo Jun 5, 2025
37e5058
Merge pull request #1 from MetaLex-Tech/feat/yearnBorg-whitelist-coap…
Detoo Jun 5, 2025
531f382
chore: Add CHANGELOG.md
Detoo Jun 5, 2025
24977db
feat: Change Yearn BORG oracle TTL per requested
Detoo Jun 13, 2025
b9d4338
Fix/audit items (#4)
Detoo Jun 17, 2025
3ac4613
chore: Add notes on contract verification
Detoo Jun 19, 2025
6d67712
chore: Update README to spec
Detoo Jun 25, 2025
9e0993c
test: fix broken tests due to contaminated test addresses on Ethereum…
Detoo Aug 18, 2025
83ab694
chore: improve instructions on contract verification
Detoo Aug 18, 2025
a31897b
test: add acceptance tests on production deployment on Ethereum mainn…
Detoo Sep 3, 2025
2ec32e8
test: real-world multisend txs
Detoo Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
cache/
out/
.env
.env*
test-command.txt
broadcast/

.DS_Store
.idea/
/broadcast_bk

/tmp/
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- src/implants/sudoImplant.sol
- A Module (implant) for enforcing BORG, DAO co-approvals on Safe admin operations such as toggling Modules or setting Guards

- src/libs/governance/snapShotExecutor.sol
- An off-chain (snapshot) voting coordinator contract that enforces BORG, DAO co-approvals on proposals

- scripts/yearnBorg.s.sol
- Scripts for deploying Yearn BORG contracts

### Updated

- src/borgCore.sol
- Blocks delegate calls by default in blacklist mode and allow whitelisting specific contracts
- Maintains the same behaviors in whitelist mode

- src/implants/ejectImplant.sol
- Allows admin to allow/disallow a member to reduce threshold when resigning
270 changes: 270 additions & 0 deletions README-yearnBorg.md

Large diffs are not rendered by default.

175 changes: 175 additions & 0 deletions scripts/yearnBorg.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.20;

import {Script} from "forge-std/Script.sol";
import {console2} from "forge-std/console2.sol";
import {borgCore} from "../src/borgCore.sol";
import {ejectImplant} from "../src/implants/ejectImplant.sol";
import {sudoImplant} from "../src/implants/sudoImplant.sol";
import {optimisticGrantImplant} from "../src/implants/optimisticGrantImplant.sol";
import {daoVoteGrantImplant} from "../src/implants/daoVoteGrantImplant.sol";
import {daoVetoGrantImplant} from "../src/implants/daoVetoGrantImplant.sol";
import {daoVetoImplant} from "../src/implants/daoVetoImplant.sol";
import {daoVoteImplant} from "../src/implants/daoVoteImplant.sol";
import {SignatureCondition} from "../src/libs/conditions/signatureCondition.sol";
import {BorgAuth} from "../src/libs/auth.sol";
import {SnapShotExecutor} from "../src/libs/governance/snapShotExecutor.sol";
import {SafeTxHelper} from "../test/libraries/safeTxHelper.sol";
import {IGnosisSafe, GnosisTransaction, IMultiSendCallOnly} from "../test/libraries/safe.t.sol";

contract PlaceholderFailSafeImplant {
uint256 public immutable IMPLANT_ID = 0;

error PlaceholderFailSafeImplant_UnexpectedTrigger();

function recoverSafeFunds() external pure {
revert PlaceholderFailSafeImplant_UnexpectedTrigger();
}
}

contract YearnBorgDeployScript is Script {
// Safe 1.3.0 Multi Send Call Only @ Ethereum mainnet
// https://github.com/safe-global/safe-deployments?tab=readme-ov-file
IMultiSendCallOnly multiSendCallOnly = IMultiSendCallOnly(0x40A2aCCbd92BCA938b02010E17A5b8929b49130D);

// Configs: BORG Core

IGnosisSafe ychadSafe = IGnosisSafe(0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52); // ychad.eth
string borgIdentifier = "Yearn BORG";
borgCore.borgModes borgMode = borgCore.borgModes.blacklist;
uint256 borgType = 0x3; // devBORG

// Configs: SnapShowExecutor

uint256 snapShotWaitingPeriod = 3 days;
uint256 snapShotCancelWaitingPeriod = 7 days;
uint256 snapShotPendingProposalLimit = 3;
uint256 snapShotOracleTtl = 14 days;
address oracle = 0xf00c0dE09574805389743391ada2A0259D6b7a00;

SafeTxHelper safeTxHelper;

borgCore core;
ejectImplant eject;
sudoImplant sudo;
SnapShotExecutor snapShotExecutor;

BorgAuth coreAuth;
BorgAuth executorAuth;
BorgAuth implantAuth;

/// @dev For running from `forge script`. Provide the deployer private key through env var.
function run() public returns(borgCore, ejectImplant, sudoImplant, SnapShotExecutor, GnosisTransaction[] memory) {
return run(vm.envUint("DEPLOYER_PRIVATE_KEY"));
}

/// @dev For running in tests
function run(uint256 deployerPrivateKey) public returns(borgCore, ejectImplant, sudoImplant, SnapShotExecutor, GnosisTransaction[] memory) {
console2.log("Deploy Configs:");
console2.log(" BORG name:", borgIdentifier);
console2.log(" BORG mode:", uint8(borgMode));
console2.log(" BORG type:", borgType);
console2.log(" Safe Multisig:", address(ychadSafe));
console2.log(" Snapshot waiting period (secs.):", snapShotWaitingPeriod);
console2.log(" Snapshot cancel period (secs.):", snapShotCancelWaitingPeriod);
console2.log(" Snapshot pending proposal limit:", snapShotPendingProposalLimit);

address deployerAddress = vm.addr(deployerPrivateKey);
console2.log("Deployer:", deployerAddress);

safeTxHelper = new SafeTxHelper(
ychadSafe,
multiSendCallOnly,
deployerPrivateKey // No-op. We are not supposed to sign any Safe tx here
);

vm.startBroadcast(deployerPrivateKey);

// Core

coreAuth = new BorgAuth();
core = new borgCore(coreAuth, borgType, borgMode, borgIdentifier, address(ychadSafe));

// Whitelist MultiSendCallOnly for Operation.DelegateCall
core.toggleDelegateCallContract(address(multiSendCallOnly), true);

// Restrict admin operations

// Safe.OwnerManager
core.addFullAccessOrBlockContract(address(ychadSafe));
core.addPolicyMethod(address(ychadSafe), "addOwnerWithThreshold(address,uint256)");
core.addPolicyMethod(address(ychadSafe), "removeOwner(address,address,uint256)");
core.addPolicyMethod(address(ychadSafe), "swapOwner(address,address,address)");
core.addPolicyMethod(address(ychadSafe), "changeThreshold(uint256)");

// Safe.GuardManager
core.addPolicyMethod(address(ychadSafe), "setGuard(address)");

// Safe.ModuleManager
core.addPolicyMethod(address(ychadSafe), "enableModule(address)");
core.addPolicyMethod(address(ychadSafe), "disableModule(address,address)");

// Create SnapShotExecutor

executorAuth = new BorgAuth();
snapShotExecutor = new SnapShotExecutor(executorAuth, address(oracle), snapShotWaitingPeriod, snapShotCancelWaitingPeriod, snapShotPendingProposalLimit, snapShotOracleTtl);

// Add modules

implantAuth = new BorgAuth();
eject = new ejectImplant(
implantAuth,
address(ychadSafe),
address(new PlaceholderFailSafeImplant()), // Placeholder because Yearn BORG does not use failSafe
true, // _allowManagement
true, // _allowEjection
false // _allowSelfEjectReduce
);
sudo = new sudoImplant(
implantAuth,
address(ychadSafe)
);

// Transfer core ownership to SnapShotExecutor
coreAuth.updateRole(address(snapShotExecutor), implantAuth.OWNER_ROLE());
coreAuth.zeroOwner();

// Transfer executor ownership to ychad.eth
executorAuth.updateRole(address(ychadSafe), executorAuth.OWNER_ROLE());
executorAuth.zeroOwner();

// Transfer eject implant ownership to SnapShotExecutor
implantAuth.updateRole(address(snapShotExecutor), implantAuth.OWNER_ROLE());
implantAuth.zeroOwner();

vm.stopBroadcast();

console2.log("Deployed addresses:");
console2.log(" Core: ", address(core));
console2.log(" Eject Implant: ", address(eject));
console2.log(" Sudo Implant: ", address(sudo));
console2.log(" SnapShotExecutor: ", address(snapShotExecutor));
console2.log(" Core Auth: ", address(coreAuth));
console2.log(" Executor Auth: ", address(executorAuth));
console2.log(" Implant Auth: ", address(implantAuth));

// Prepare Safe TXs for ychad.eth to execute

GnosisTransaction[] memory safeTxs = new GnosisTransaction[](3);
safeTxs[0] = safeTxHelper.getAddModuleData(address(eject));
safeTxs[1] = safeTxHelper.getAddModuleData(address(sudo));
safeTxs[2] = safeTxHelper.getSetGuardData(address(core)); // Note we must set guard last because it may block ychad.eth from adding any more modules

console2.log("Safe TXs:");
for (uint256 i = 0 ; i < safeTxs.length ; i++) {
console2.log(" #", i);
console2.log(" to:", safeTxs[i].to);
console2.log(" value:", safeTxs[i].value);
console2.log(" data:");
console2.logBytes(safeTxs[i].data);
console2.log("");
}

return (core, eject, sudo, snapShotExecutor, safeTxs);
}
}
94 changes: 94 additions & 0 deletions scripts/yearnBorgReplaceSnapShotExecutor.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.20;

import {CommonBase} from "forge-std/Base.sol";
import {Script} from "forge-std/Script.sol";
import {StdChains} from "forge-std/StdChains.sol";
import {StdCheatsSafe} from "forge-std/StdCheats.sol";
import {StdUtils} from "forge-std/StdUtils.sol";
import {console2} from "forge-std/console2.sol";
import {ejectImplant} from "../src/implants/ejectImplant.sol";
import {sudoImplant} from "../src/implants/sudoImplant.sol";
import {BorgAuth} from "../src/libs/auth.sol";
import {SnapShotExecutor} from "../src/libs/governance/snapShotExecutor.sol";
import {IGnosisSafe} from "../test/libraries/safe.t.sol";

contract YearnBorgReplaceSnapShotExecutorScript is Script {

// Warning: review and update the following before run

IGnosisSafe ychadSafe = IGnosisSafe(0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52); // ychad.eth

ejectImplant eject = ejectImplant(0xe44f5c9EAFB87731906AB87156E4F4cB3fa0Eb74);
sudoImplant sudo = sudoImplant(0x6766b727aa1489443b34A02ee89c34f39748600b);
SnapShotExecutor oldSnapShotExecutor = SnapShotExecutor(0x77691936fb6337d4B71dc62643b05b6bBE19285c);

// Configs: SnapShowExecutor
// Reuse the old one's parameters if we are just upgrading it to a newer version
uint256 snapShotWaitingPeriod = oldSnapShotExecutor.waitingPeriod();
uint256 snapShotCancelWaitingPeriod = oldSnapShotExecutor.cancelWaitingPeriod();
uint256 snapShotPendingProposalLimit = oldSnapShotExecutor.pendingProposalLimit();
uint256 snapShotOracleTtl = oldSnapShotExecutor.oracleTtl();
address oracle = oldSnapShotExecutor.oracle();

BorgAuth executorAuth = oldSnapShotExecutor.AUTH();
BorgAuth implantAuth = eject.AUTH();

/// @dev For running from `forge script`. Provide the deployer private key through env var.
function run() public returns(SnapShotExecutor, bytes memory, bytes memory) {
return run(vm.envUint("DEPLOYER_PRIVATE_KEY"));
}

/// @dev For running in tests
function run(uint256 deployerPrivateKey) public returns(SnapShotExecutor, bytes memory, bytes memory) {
console2.log("Configs:");
console2.log(" Safe Multisig:", address(ychadSafe));
console2.log(" Eject Implant:", address(eject));
console2.log(" Sudo Implant:", address(sudo));
console2.log(" Old SnapShotExecutor:", address(oldSnapShotExecutor));

address deployerAddress = vm.addr(deployerPrivateKey);
console2.log("Deployer:", deployerAddress);

vm.startBroadcast(deployerPrivateKey);

// Deploy new SnapShotExecutor
SnapShotExecutor newSnapShotExecutor = new SnapShotExecutor(executorAuth, address(oracle), snapShotWaitingPeriod, snapShotCancelWaitingPeriod, snapShotPendingProposalLimit, snapShotOracleTtl);

vm.stopBroadcast();

console2.log("Deployed addresses:");
console2.log(" New SnapShotExecutor: ", address(newSnapShotExecutor));

// Generate the proposal calldata for old SnapShotExecutor to transfer its implant ownership to the new one.
// We can't just do it here. The proposal must go through the co-approval process to take effect.

bytes memory grantNewOwnerData = abi.encodeWithSelector(
implantAuth.updateRole.selector,
address(newSnapShotExecutor),
implantAuth.OWNER_ROLE()
);

bytes memory revokeOldOwnerData = abi.encodeWithSelector(
implantAuth.updateRole.selector,
address(oldSnapShotExecutor),
0
);

console2.log("Tx proposal for the old SnapShotExecutor:");
console2.log(" to:", address(implantAuth));
console2.log(" value: 0");
console2.log(" data:");
console2.logBytes(grantNewOwnerData);
console2.log("");

console2.log("Tx proposal for the new SnapShotExecutor:");
console2.log(" to:", address(implantAuth));
console2.log(" value: 0");
console2.log(" data:");
console2.logBytes(revokeOldOwnerData);
console2.log("");

return (newSnapShotExecutor, grantNewOwnerData, revokeOldOwnerData);
}
}
34 changes: 27 additions & 7 deletions src/borgCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,15 @@ contract borgCore is BaseGuard, BorgAuthACL, IEIP4824 {
string private _daoUri; // URI for the DAO
LegalAgreement[] public legalAgreements; // array of legal agreements URIs for this BORG
string public constant VERSION = "1.0.0"; // contract version

// 0x1 securityBORG/eBORG
// 0x2 grantsBORG
// 0x3 devBORG
// 0x4 finBORG
// 0x5 genBORG
// 0x6 bzBORG
uint256 public immutable borgType; // type of the BORG

enum borgModes {
whitelist, // everything is restricted except what has been whitelisted
blacklist, // everything is allowed except contracts and methods that have been blacklisted. Param checks work the same as whitelist
Expand Down Expand Up @@ -188,13 +196,18 @@ contract borgCore is BaseGuard, BorgAuthACL, IEIP4824 {
lastNativeExecutionTimestamp = block.timestamp;
}

// Block all delegate calls by default in blacklist mode
if(operation == Enum.Operation.DelegateCall) {
// Only allow if contract is explicitly whitelisted for delegate calls
if(!policy[to].enabled || !policy[to].delegateCallAllowed) {
revert BORG_CORE_DelegateCallNotAuthorized();
}
}

//black list contract calls w/ data
if (data.length > 0) {
if(policy[to].enabled) {
if(policy[to].fullAccessOrBlock) revert BORG_CORE_InvalidContract();
if(!policy[to].delegateCallAllowed && operation == Enum.Operation.DelegateCall) {
revert BORG_CORE_DelegateCallNotAuthorized();
}

if(!isMethodCallAllowed(to, data))
revert BORG_CORE_MethodNotAuthorized();
Expand Down Expand Up @@ -297,12 +310,19 @@ contract borgCore is BaseGuard, BorgAuthACL, IEIP4824 {
/// @param _contract address, the address of the contract
/// @param _allowed bool, the flag to allow delegate calls
function toggleDelegateCallContract(address _contract, bool _allowed) external onlyOwner {
//ensure the contract is allowed before enabling delegate calls
//ensure the contract is allowed before enabling delegate calls
if(policy[_contract].enabled == true)
{
policy[_contract].delegateCallAllowed = _allowed;
emit DelegateCallToggled(_contract, _allowed);
}
else if(borgMode == borgModes.blacklist)
{
// Toggle will enable the contract policy
policy[_contract].enabled = true;
policy[_contract].delegateCallAllowed = _allowed;
emit DelegateCallToggled(_contract, _allowed);
}
else
revert BORG_CORE_InvalidContract();
}
Expand Down Expand Up @@ -641,12 +661,12 @@ contract borgCore is BaseGuard, BorgAuthACL, IEIP4824 {
bytes4 methodSelector = bytes4(_methodCallData[:4]);
MethodConstraint storage methodConstraint = policy[_contract].methods[methodSelector];

if (!methodConstraint.enabled && borgMode == borgModes.whitelist)
if (!methodConstraint.enabled && borgMode == borgModes.whitelist)
return false;


if(methodConstraint.enabled && methodConstraint.paramOffsets.length == 0 && borgMode == borgModes.blacklist)
return false;
return false;

// Iterate through the whitelist constraints for the method
for (uint256 i = 0; i < methodConstraint.paramOffsets.length;) {
Expand Down
Loading