Skip to content

feat: AdminBurnExecutor — auditable one-shot scheme for burning illegitimate G$ - #305

Open
blueogin wants to merge 2 commits into
masterfrom
feat/admin-burn-executor-scheme
Open

feat: AdminBurnExecutor — auditable one-shot scheme for burning illegitimate G$#305
blueogin wants to merge 2 commits into
masterfrom
feat/admin-burn-executor-scheme

Conversation

@blueogin

@blueogin blueogin commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds the on-chain executor and the operational scripts for running an adminBurn
sweep against a list of addresses holding illegitimate G$, plus the USD refund
ledger the sweep produces.

About # (link your issue here)

How Has This Been Tested?

Please describe the tests that you ran to verify your changes.

Checklist:

  • PR title matches follow: (Feature|Bug|Chore) Task Name
  • My code follows the style guidelines of this project
  • I have followed all the instructions described in the initial task (check Definitions of Done)
  • I have performed a self-review of my own code
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have added reference to a related issue in the repository
  • I have added a detailed description of the changes proposed in the pull request. I am as descriptive as possible, assisting reviewers as much as possible.
  • I have added screenshots related to my pull request (for frontend tasks)
  • I have pasted a gif showing the feature.
  • @mentions of the person or team responsible for reviewing proposed changes

Summary by Sourcery

Introduce an auditable AdminBurnExecutor workflow for sweeping illegitimate G$ and generating the corresponding off-chain USD refund ledger.

New Features:

  • Add an auditable, one-shot executor for burning specified illegitimate G$ balances while recording per-account USD refund obligations.
  • Add deployment and execution tooling for validating burn lists, coordinating guardian scheme registration, running retryable or terminal sweeps, and producing refund ledgers.

Enhancements:

  • Support best-effort burns with failure reporting, balance-effect verification, resumable pending entries, cancellation, and automatic permission relinquishment.
  • Provide dry-run, deployment-only, print-only, free-list, and signer validation workflows for safer operational execution.

Deployment:

  • Add scripts for deploying, verifying, registering, and executing AdminBurnExecutor across supported networks.

Chores:

  • Add an example burn-list configuration for operational use.

Copilot AI lite review requested due to automatic review settings September 9, 2026 16:26

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 security issue, and 2 other issues

Security issues:

  • Detected calls to child_process from a function argument address. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed. (link)
Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="scripts/upgrades/admin-burn-executor-deploy.ts" line_range="128-141" />
<code_context>
+    throw new Error(`token owner ${owner} is not the Avatar ${release.Avatar} - adminBurn would revert`);
+
+  // the executor is useless against an implementation that has no adminBurn
+  if (!supergd.interface.functions["adminBurn(address,uint256)"])
+    throw new Error("local artifacts have no adminBurn(address,uint256) - run `yarn compile`");
+  // ...and against a *live* implementation that has no adminBurn either. The
+  // proxy would delegate the call into an implementation that reverts, so check
</code_context>
<issue_to_address>
**issue (bug_risk):** The deployment script always throws because the diff does not add `adminBurn(address,uint256)` to the `SuperGoodDollar` artifact or implementation. `supergd.interface.functions[...]` is therefore undefined, so no executor can be deployed through this script.

**Triggers:** When this PR is compiled against the repository as shown.

**Suggested fix:** Add the `adminBurn` implementation and update the deployed implementation before relying on this script, or remove this check until that token change is included.

```suggestion

```
</issue_to_address>

### Comment 2
<location path="scripts/upgrades/admin-burn-executor-deploy.ts" line_range="205-206" />
<code_context>
+    totalRefundUSD: ethers.utils.formatEther(onChainUSD),
+    owner: await executor.owner()
+  });
+  if (!onChainGD.eq(totalGD) || !onChainUSD.eq(totalUSD) || !count.eq(entries.length))
+    throw new Error("deployed executor does not match the local burn list");
+
+  // not registered yet, so canExecute is expected to be false on the scheme check
</code_context>
<issue_to_address>
**issue (broader_impact):** When `EXECUTOR` points to an existing executor, the script verifies only the aggregate G$ total, refund total, and entry count. A different fixed list with the same aggregates passes validation, so the script can preflight and execute an unaudited set of accounts or amounts while presenting the local list as the target.

**Triggers:** When an existing executor has the same entry count and aggregate totals as the local burn list but different tuples.

**Suggested fix:** Compare every on-chain `(account, gdAmount, refundUSD)` entry against the locally loaded list in order, and reject any mismatch.
</issue_to_address>

### Comment 3
<location path="scripts/upgrades/admin-burn-executor-deploy.ts" line_range="302" />
<code_context>
    execSync(cmd, { stdio: "inherit" });
</code_context>
<issue_to_address>
**security (javascript.lang.security.detect-child-process):** Detected calls to child_process from a function argument `address`. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

*Source: opengrep*
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 3 findings to address first, and once the scheme is registered and executed, the fixed list can permanently burn token balances through the Controller's genericCall authority, and an incorrect list cannot be undone by reverting the deployment. The recorded refund ledger can also create off-chain compensation obligations, while the authorization and target list are themselves the irreversible decision.

Blocking findings: scripts/upgrades/admin-burn-executor-deploy.ts:141, scripts/upgrades/admin-burn-executor-deploy.ts:206, scripts/upgrades/admin-burn-executor-deploy.ts:302


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +128 to +141
// the executor is useless against an implementation that has no adminBurn
if (!supergd.interface.functions["adminBurn(address,uint256)"])
throw new Error("local artifacts have no adminBurn(address,uint256) - run `yarn compile`");
// ...and against a *live* implementation that has no adminBurn either. The
// proxy would delegate the call into an implementation that reverts, so check
// the selector is actually present in the deployed code.
const selector = supergd.interface.getSighash("adminBurn(address,uint256)");
const liveImpl = await supergd.getCodeAddress();
const liveCode = await ethers.provider.getCode(liveImpl);
if (!liveCode.includes(selector.slice(2)) && !process.env.SKIP_IMPL_CHECK)
throw new Error(
`live SuperGoodDollar implementation ${liveImpl} has no adminBurn - run supergooddollar-admin-burn.ts first`
);
console.log("live implementation:", liveImpl, "(adminBurn present)");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The deployment script always throws because the diff does not add adminBurn(address,uint256) to the SuperGoodDollar artifact or implementation. supergd.interface.functions[...] is therefore undefined, so no executor can be deployed through this script.

Triggers: When this PR is compiled against the repository as shown.

Suggested fix: Add the adminBurn implementation and update the deployed implementation before relying on this script, or remove this check until that token change is included.

Suggested change
// the executor is useless against an implementation that has no adminBurn
if (!supergd.interface.functions["adminBurn(address,uint256)"])
throw new Error("local artifacts have no adminBurn(address,uint256) - run `yarn compile`");
// ...and against a *live* implementation that has no adminBurn either. The
// proxy would delegate the call into an implementation that reverts, so check
// the selector is actually present in the deployed code.
const selector = supergd.interface.getSighash("adminBurn(address,uint256)");
const liveImpl = await supergd.getCodeAddress();
const liveCode = await ethers.provider.getCode(liveImpl);
if (!liveCode.includes(selector.slice(2)) && !process.env.SKIP_IMPL_CHECK)
throw new Error(
`live SuperGoodDollar implementation ${liveImpl} has no adminBurn - run supergooddollar-admin-burn.ts first`
);
console.log("live implementation:", liveImpl, "(adminBurn present)");

Comment on lines +205 to +206
if (!onChainGD.eq(totalGD) || !onChainUSD.eq(totalUSD) || !count.eq(entries.length))
throw new Error("deployed executor does not match the local burn list");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (broader_impact): When EXECUTOR points to an existing executor, the script verifies only the aggregate G$ total, refund total, and entry count. A different fixed list with the same aggregates passes validation, so the script can preflight and execute an unaudited set of accounts or amounts while presenting the local list as the target.

Triggers: When an existing executor has the same entry count and aggregate totals as the local burn list but different tuples.

Suggested fix: Compare every on-chain (account, gdAmount, refundUSD) entry against the locally loaded list in order, and reject any mismatch.

const cmd = `yarn hardhat verify --contract contracts/utils/AdminBurnExecutor.sol:AdminBurnExecutor --constructor-args ${argsFile} ${address} --network ${network.name}`;
console.log("\n=== verifying ===\n" + cmd);
try {
execSync(cmd, { stdio: "inherit" });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (javascript.lang.security.detect-child-process): Detected calls to child_process from a function argument address. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

Source: opengrep

@blueogin
blueogin requested a review from sirpy September 9, 2026 16:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are confirmed correctness issues in the operational scripts (BigNumber underflow in the ledger summary and fork-network simulation detection), plus missing automated test coverage for a high-privilege burn scheme.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds an on-chain, fixed-list “AdminBurnExecutor” scheme plus operational Hardhat scripts to deploy, register (via guardians Safe), and execute an auditable one-shot adminBurn sweep while emitting/writing a per-account USD refund ledger.

Changes:

  • Introduces contracts/utils/AdminBurnExecutor.sol, a one-time DAOStack scheme that burns a constructor-fixed list and self-unregisters (or can be cancelled).
  • Adds deployment tooling to validate a burn list, deploy/verify an executor, and propose Controller.registerScheme(..., genericCall) via Safe.
  • Adds an execution script to preflight, execute (or cancel), and output a JSON refund ledger.
File summaries
File Description
scripts/upgrades/admin-burn-list.example.json Example burn-list input format (human units + wei variants).
scripts/upgrades/admin-burn-executor-deploy.ts Validates list, deploys/verifies executor, proposes scheme registration via Safe/guardian simulation.
scripts/upgrades/admin-burn-executor-execute.ts Preflights and executes/cancels the executor, then generates a refund ledger from emitted events.
contracts/utils/AdminBurnExecutor.sol New fixed-list, one-shot burn scheme that uses genericCall to invoke adminBurn and then unregisters itself.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread contracts/utils/AdminBurnExecutor.sol Outdated
* All or nothing: if a single burn fails, the whole transaction reverts and
* the scheme stays armed so it can be retried.
*/
function execute() external {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blueogin should try every address. emit event for failed addresses.

let { name: networkName } = network;
networkName = networkName.replace("-fork", "");

const isSimulation = ["hardhat", "fork", "localhost"].includes(network.name);
totalRefundUSD: ethers.utils.formatEther(totalUSD),
supplyBefore: ethers.utils.formatEther(supplyBefore),
supplyAfter: ethers.utils.formatEther(supplyAfter),
supplyDelta: ethers.utils.formatEther(supplyAfter.sub(supplyBefore)),
Comment thread contracts/utils/AdminBurnExecutor.sol Outdated
* Flow:
* 1. deploy with the full list
* 2. guardians register this address as a scheme (genericCall permission)
* 3. anyone (or `owner`, see `execute`) calls `execute()`
Comment thread contracts/utils/AdminBurnExecutor.sol Outdated
Comment on lines +102 to +103
* Guardians can call this before signing the scheme registration, and it is
* re-checked inside `execute`.
Comment thread contracts/utils/AdminBurnExecutor.sol Outdated
* All or nothing: if a single burn fails, the whole transaction reverts and
* the scheme stays armed so it can be retried.
*/
function execute() external {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blueogin should try every address. emit event for failed addresses.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants