Skip to content

feat(APP-1099): Implement Safe-backed SPP proposal bodies - #1397

Open
thekidnamedkd wants to merge 130 commits into
mainfrom
safe-biz-logic
Open

thekidnamedkd wants to merge 130 commits into
mainfrom
safe-biz-logic

Conversation

@thekidnamedkd

@thekidnamedkd thekidnamedkd commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Description

Implements APP-1099. This PR now targets main and includes the Safe account work from #1336. Native Safe processes in #1437 stack on this branch.

  • Safe-backed SPP bodies sign approvals and vetoes as Safe transactions through the Safe
    proxy: propose → shared nonce-ordered queue → co-sign → threshold → bundled execution →
    report recorded → stage progression.
  • Shared nonce queue integration: the account queue renders in nonce order, keeps
    same-nonce rivals in service order, and the proposal card routes to the account queue while a
    live report occupies a slot (and offers no dead deep-link past it).
  • Supersede / re-queue: a report that loses its nonce flips to SUPERSEDED, its lost
    confirmations stay visible as history, and "approve & re-queue" rebuilds a fresh envelope at a
    new nonce without reusing dead signatures.
  • Read-only Safe account: inspect balances and queued transactions, follow associated
    proposal cards, open an independent payload/hash review, or use the queued timestamp to open
    the transaction in Safe. Signing, confirmation and execution stay in proposal context; the
    account exposes no removal or replacement actions.
  • Independent review surface (RFP‑4): before signing, the UI shows the decoded calldata and
    the EIP-712 domain hash, message hash and safeTxHash, matching what the hardware wallet and an
    independent tool show.
  • State correctness: nonce order, nonce rivals, staleness and the withheld hash pair;
    numeric envelope-field guards; batch-operation recognition; threshold trusted from the Safe
    itself; late-report and maxAdvance active/expired semantics; spent read-budget warning;
    ownership gates and standard wallet + permission dialogs.
  • Release notes consolidated (safe-accounts.md, safe-spp-signing-trust.md).

Backend Safe body/process records, owners and transaction endpoints are now available on dev. The earlier missing-Safe-record blocker no longer applies. Queue cards use aragonReports for proposal navigation, never as execution authority.

Verification updated 2026-10-16

  • A real Sepolia body veto completed in one wallet transaction on the earlier stacked preview at 0fc944385: transaction 0x2e6360…7e7c. The Safe success event, nonce advancement, executed history and indexed VETO result matched. The proposal displayed Vetoed after reload.
  • That run exposed a frontend dialog handoff bug. Commit bdac781f6 fixes Execute overriding the indexing step. A failing-before/passing-after regression and a browser recovery check with the same mined receipt verified indexing through Done, without another transaction.
  • Focused body checks passed 130 tests across six suites. Current-head CI passes, including tests, deployment, E2E and CodeQL. Rejected network-switch recovery also has browser coverage.
  • Evan's September 29 feedback on APP-1099 approved the voting terminal and asked for Sign + Execute to be one operation. The live flow above exercised that operation.

A live shared-Safe/linked-DAO fixture remains unavailable; identity separation, member selection and signer links have deterministic coverage. Backend freshness questions remain, but these checks found no missing backend feature blocking the ticket. Review approval and the existing review threads are still open.

Type of Change

  • Major: Breaking change (change that would cause existing functionality to not work as expected)
  • Minor: Feature (non-breaking change which adds new functionality)
  • Patch: Enhancement (non-breaking change to an existing feature)
  • Patch: Bug fix (non-breaking change which fixes an issue)

Developer Checklist:

  • Manually smoke tested the functionality in a preview or locally
  • Confirmed there are no new warnings or errors in the browser console
  • (For User Stories only) Double-checked that all Acceptance Criteria are satisfied
  • Confirmed there are no new warnings on automated tests
  • Merged and published any dependent changes in downstream modules
  • Selected the correct base branch
  • Commented the code in hard-to-understand areas
  • Followed the code style guidelines of this project
  • Reviewed that the Files Changed in Github’s UI reflect my intended changes
  • Confirmed the pipeline checks are not failing

Review Checklist:

  • (For User Stories only) Tested in a preview or locally that all Acceptance Criteria are satisfied
  • Confirmed that changes follow the code style guidelines of this project

# Conflicts:
#	.gitignore
#	apps/app/package.json
#	pnpm-lock.yaml
# Conflicts:
#	apps/app/package.json
#	pnpm-lock.yaml
@thekidnamedkd
thekidnamedkd marked this pull request as ready for review September 30, 2026 14:48

@milosh86 milosh86 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.

Left a couple of comments, but I have more code to check. Sorry, comments might be in a random order and probably all 3 related.

/>
</Page.Main>
<Page.Aside>
{activeSafeSource != null && (

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.

We need to rethink this change ... GOVERNANCE_MEMBER_PANEL slot now means different thing based on the body type + body type detection is in the page component itself, which mostly defeats the purpose of slots.

One option would be to introduce a new slot for the DaoPluginInfo which Safe plugin can update, and default slot implementation could be like it is now (to cover other plugins).

featuredDelegates,
});

const { activePlugin: membersPlugin, setActivePlugin: setMembersPlugin } =

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.

I'm thinking about something .... does it make sense to add safe as a plugin to a DAO with isBody: true when it is a body in a process in that DAO? ... then we would have a single safe interface type.

Also, in a workspace, a Safe account would always be treated as both process and a body, but this is a bit different than case when safe is a body in a DAO's SPP process.

const urlParam = searchParams.get(daoMemberListFilterParam);

const { activePlugin, setActivePlugin, plugins } =
useDaoPluginFilterUrlParam({

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.

this is also related to previous comment ... if we have safes as real body plugins we wouldn't need to do this complex search and filtering.

btw, if we remove useDaoPluginFilterUrlParam don't we loose tab state persistance?

@thekidnamedkd

thekidnamedkd commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

All three at f2e16b986.

Went with one safe interface type: added PluginInterfaceType.SAFE, deleted safeBodyPluginId = 'external-safe', registered Safe as a real plugin and re-keyed its slots onto it. The enum entry turned out to be load-bearing, since daoUtils derives its supported-interface set from it and canonical safe records were being dropped as unknown before.

With Safes as real body plugins the complex search and filtering went away: the SPP-settings scan, the process-list lookup and the synthetic sources are gone, and Members reads ordinary body records.

On tab state, removing useDaoPluginFilterUrlParam didn't cost it, PluginFilterComponent already drives useFilterUrlParam. Tab ids are now ${address}-${slug}. Checked selection, reload, direct links, an invalid value (falls back to the first tab) and unrelated query params. I didn't add a compatibility layer for the old safe:<daoId>:<address> ids since they only ever existed on this branch, but say if you'd rather have one.

For the panel there's now a SETTINGS_PLUGIN_INFO slot with DaoPluginInfo as the default implementation and a Safe override. GOVERNANCE_MEMBER_PANEL keeps its old meaning across plugins, and the body-type branch is out of the page component rather than moved into a helper.

One thing that came with sharing the registry id: SPP stage bodies now resolve their proposal-creation check from the stage condition instead of the resolved registry id, so an external stage reference can't land on the standalone Safe owner + Execute guard. Unsupported-network Safes still go through the external fallback.

type-check 9/9, 452 suites / 3200 tests, Biome clean on the changed files.

Blocker for merge rather than review: dev returns no interfaceType: 'safe' records at all, and the three DAOs that use a Safe reference it only as a nested SPP stage body, so Members shows no Safe tab today. That needs the isBody: true records (plus isProcess: true where Execute-associated) and a backfill before this is releasable.

@thekidnamedkd
thekidnamedkd requested a review from milosh86 October 7, 2026 12:32

@milosh86 milosh86 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 checked the last couple of commits addressing previous comments and it looks good, feels more natural 👍

Now in the main PR I see a bunch of code which seems unrelated to the main goal. I suspect it is now leftover after last refactor which AI still kept. Please double check this, I left some comments but I guess there are more places with the same thing.

? featuredDelegatesTabId
: (membersPlugin?.uniqueId ?? '');
const membersPageUrl = `${daoUrl}/members?${daoMembersPageFilterParam}=${membersTabParam}`;
: searchParams.get(daoDashboardPageMembersFilterParam);

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.

Is this change now needed?


const { activePlugin, setActivePlugin, plugins } =
useDaoPluginFilterUrlParam({
const { data: dao } = useDao({

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.

same here, is this change still needed? It seems this was needed only when safe was not a body plugin?


if (resolvedDaoId === daoId) {
return initialParams;
if (memberSource != null) {

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.

check this file too please

> = async (props) => {
const { params } = props;
const { address: rawAddress, addressOrEns, network } = await params;
const { params, searchParams } = props;

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.

this file too.

IDaoMemberDetailsPageClientProps
> = (props) => {
const { address, daoId, network, pluginAddress, tokenAddress } = props;
const {

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.

this file too

);

if (!plugins.length) {
const memberSources = daoMemberSourceUtils.resolve({

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.

this file too....please move to another PR if still needed.

This branch was successfully deployed

1 active deployment
preview — bdac781f Deployed Oct 15, 2026 by thekidnamedkd via deploy / deploy #5570
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.

2 participants