Skip to content

feat: 4055 Implement Owner Management for Smart Contracts#5655

Merged
Pyatakov merged 10 commits intohashgraph:developfrom
nosid91:dimaredchyts/feat-4055-Lack-of-Method-to-Change-OWNER-Role
Feb 27, 2026
Merged

feat: 4055 Implement Owner Management for Smart Contracts#5655
Pyatakov merged 10 commits intohashgraph:developfrom
nosid91:dimaredchyts/feat-4055-Lack-of-Method-to-Change-OWNER-Role

Conversation

@nosid91
Copy link
Copy Markdown
Contributor

@nosid91 nosid91 commented Jan 26, 2026

Description

This PR implements a secure mechanism to transfer and manage the OWNER role in Guardian smart contracts to address security audit findings (#4055).

  • Add two-step ownership transfer pattern in Access.sol (proposeOwner, claimOwner, removeOwner)
  • Add Guardian CLI commands for owner management (propose-owner, claim-owner, remove-owner)
  • Add comprehensive test suite for ownership transfer scenarios
  • Document owner management workflow in contracts README

Related issue(s)

Fixes #4055

Notes for Reviewer

Why Two-Step Ownership Transfer?

The implementation uses a two-step ownership transfer pattern (propose + claim) instead of direct transfer for the following security reasons:

  1. Prevents accidental transfers: Direct ownership transfer to an incorrect address (typos, copy-paste errors) would result in permanent loss of contract control. The two-step pattern requires the new owner to actively claim ownership, confirming they have access to the destination address.

  2. Validates recipient capability: The claim step verifies that the new owner can actually execute transactions, ensuring they have the private key and proper account setup.

  3. Provides recovery window: If an owner accidentally proposes the wrong address, they can simply propose a different address to overwrite the pending owner before the claim occurs.

  4. Industry standard: This pattern is widely adopted in production contracts (OpenZeppelin's Ownable2Step, major DeFi protocols) as a security best practice.

Security Constraints

  • Owners cannot remove themselves (prevents accidental lockout)
  • Contract address cannot be removed as owner (maintains internal call capability)
  • pendingOwner is cleared after successful claim (prevents replay)

Backwards Compatibility

  • Existing deployed contracts are not affected
  • No changes to existing public interfaces

Checklist

  • Documented (Code comments, README, CLI help)
  • Tested (integration tests with Hedera local node)

@nosid91 nosid91 requested review from a team as code owners January 26, 2026 09:31
…nds.

Signed-off-by: dima <nosid91@gmail.com>
@nosid91 nosid91 force-pushed the dimaredchyts/feat-4055-Lack-of-Method-to-Change-OWNER-Role branch from e064636 to a37f098 Compare January 26, 2026 09:31
@Pyatakov Pyatakov self-assigned this Jan 27, 2026
@Pyatakov Pyatakov changed the title feat: Implement Owner Management for Smart Contracts feat: 4055 Implement Owner Management for Smart Contracts Jan 27, 2026
@anvabr anvabr deleted the branch hashgraph:develop January 30, 2026 20:17
@anvabr anvabr closed this Jan 30, 2026
@Pyatakov Pyatakov reopened this Jan 31, 2026
Copy link
Copy Markdown
Contributor

@andrewb1269 andrewb1269 left a comment

Choose a reason for hiding this comment

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

Approve the two package.json files. Please ensure other folks review the other files in the PR. Thank you!

Comment thread contracts/README.md
Comment on lines +164 to +166
## Owner Management

The smart contracts use a two-step ownership transfer pattern for security. This allows safe transfer of ownership without the risk of losing access to the contract.
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.

Putting a block here until another reviewer also approves. Please @ me and I can remove the block once another reviewer approves the PR.

@kirill-tolochko
Copy link
Copy Markdown

Hello. I was checking your PR 5655 and found that there are errors after Wipe contract creation, and Retire contract cannot be created. I tried to create both in UI. There is a error like this:

2026-02-09T13:43:02.133Z [GUARDIAN_SERVICE]: TypeError: no matching event (argument="key", value="0xbca31226f48d9d9a4c4759ba05c7e55d4d155ef264dd1e7f462df37b89647fa9", code=INVALID_ARGUMENT, version=6.15.0)
at makeError (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/utils/errors.js:125:21)
at assert (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/utils/errors.js:151:15)
at assertArgument (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/utils/errors.js:162:5)
at Interface.getEventName (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/interface.js:527:9)
at syncWipeContract (file:///C:/guardian/guardian-service/dist/api/contract.service.js:327:40)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async Object.syncWipeContracts (file:///C:/guardian/guardian-service/dist/api/contract.service.js:298:9)
at async CronJob.taskExecution (file:///C:/guardian/guardian-service/dist/helpers/synchronization-task.js:56:29)

Also I've found that this issue was already presented in the 4054 story commit dd244b1

Could you please take a look?

Steps:

  1. deploy new Wipe, Retire, RetireSingleToken, RetireDoubleToken

  2. Update contracts ids in the .env.guardian
    RETIRE_CONTRACT_FILE_ID="0.0.6371646"
    WIPE_CONTRACT_FILE_ID="0.0.6371642"
    RETIRE_SINGLE_FILE_ID="0.0.6371651"
    RETIRE_DOUBLE_FILE_ID="0.0.6371652"

  3. Restart guardian service (if it is running already or just start everything), try to create new wipe contract and new retire contract

@nosid91 nosid91 closed this Feb 11, 2026
@nosid91 nosid91 reopened this Feb 11, 2026
@Pyatakov Pyatakov deleted the branch hashgraph:develop February 17, 2026 21:33
@Pyatakov Pyatakov closed this Feb 17, 2026
@nosid91
Copy link
Copy Markdown
Contributor Author

nosid91 commented Feb 18, 2026

@Pyatakov Hello, I would like to know why it was closed? Regarding the bug, I found the cause - manual ABI generation. I fixed it in the commit nosid91@826dcbe. I did not respond because I did not have time to fully test the above to double-check its correctness. I planned to set aside time for this this week. Sorry for the delay.

@Pyatakov Pyatakov reopened this Feb 18, 2026
@kirill-tolochko
Copy link
Copy Markdown

Hi @nosid91
I tried with this fix and there is different error in the guardian service. And still cannot create Retire contract:

2026-02-18T12:17:00.659Z [GUARDIAN_SERVICE]: RangeError: data out-of-bounds (buffer=0x, length=0, offset=32, code=BUFFER_OVERRUN, version=6.15.0)
at makeError (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/utils/errors.js:129:21)
at assert (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/utils/errors.js:151:15)
at #peekBytes (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/abstract-coder.js:435:17)
at Reader.readBytes (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/abstract-coder.js:452:36)
at Reader.readValue (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/abstract-coder.js:460:30)
at AddressCoder.decode (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/address.js:26:42)
at file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/array.js:82:31
at Array.forEach ()
at unpack (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/array.js:61:12)
at TupleCoder.decode (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/tuple.js:60:16)
at AbiCoder.decode (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/abi-coder.js:180:22)
at Interface.decodeEventLog (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/interface.js:988:49)
at syncWipeContract (file:///C:/guardian/guardian-service/dist/api/contract.service.js:328:35)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async Object.syncWipeContracts (file:///C:/guardian/guardian-service/dist/api/contract.service.js:298:9)
at async CronJob.taskExecution (file:///C:/guardian/guardian-service/dist/helpers/synchronization-task.js:56:29)

Signed-off-by: dima <nosid91@gmail.com>
The Access.sol contract declares events with `indexed` parameters
(OwnerAdded, OwnerRemoved, OwnerProposed), but the TypeScript ABI
was missing `indexed` and decodeEventLog was called without topics.
This caused BUFFER_OVERRUN when decoding events with empty log.data.

Signed-off-by: dima <nosid91@gmail.com>
@nosid91 nosid91 force-pushed the dimaredchyts/feat-4055-Lack-of-Method-to-Change-OWNER-Role branch from 826dcbe to 7263c96 Compare February 19, 2026 08:29
The ownership management functions (proposeOwner, claimOwner, removeOwner)
increased contract bytecode size, causing INSUFFICIENT_GAS on deployment.
Updated gas defaults based on actual testnet usage:
- RETIRE_SINGLE_CONTRACT_GAS: 6M -> 7M (actual: 6.3M)
- RETIRE_DOUBLE_CONTRACT_GAS: 7M -> 8M (actual: 7.3M)

Signed-off-by: dima <nosid91@gmail.com>
@nosid91
Copy link
Copy Markdown
Contributor Author

nosid91 commented Feb 19, 2026

Hi @nosid91 I tried with this fix and there is different error in the guardian service. And still cannot create Retire contract:

2026-02-18T12:17:00.659Z [GUARDIAN_SERVICE]: RangeError: data out-of-bounds (buffer=0x, length=0, offset=32, code=BUFFER_OVERRUN, version=6.15.0) at makeError (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/utils/errors.js:129:21) at assert (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/utils/errors.js:151:15) at #peekBytes (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/abstract-coder.js:435:17) at Reader.readBytes (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/abstract-coder.js:452:36) at Reader.readValue (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/abstract-coder.js:460:30) at AddressCoder.decode (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/address.js:26:42) at file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/array.js:82:31 at Array.forEach () at unpack (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/array.js:61:12) at TupleCoder.decode (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/tuple.js:60:16) at AbiCoder.decode (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/abi-coder.js:180:22) at Interface.decodeEventLog (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/interface.js:988:49) at syncWipeContract (file:///C:/guardian/guardian-service/dist/api/contract.service.js:328:35) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async Object.syncWipeContracts (file:///C:/guardian/guardian-service/dist/api/contract.service.js:298:9) at async CronJob.taskExecution (file:///C:/guardian/guardian-service/dist/helpers/synchronization-task.js:56:29)

Hi @nosid91 I tried with this fix and there is different error in the guardian service. And still cannot create Retire contract:

2026-02-18T12:17:00.659Z [GUARDIAN_SERVICE]: RangeError: data out-of-bounds (buffer=0x, length=0, offset=32, code=BUFFER_OVERRUN, version=6.15.0) at makeError (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/utils/errors.js:129:21) at assert (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/utils/errors.js:151:15) at #peekBytes (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/abstract-coder.js:435:17) at Reader.readBytes (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/abstract-coder.js:452:36) at Reader.readValue (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/abstract-coder.js:460:30) at AddressCoder.decode (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/address.js:26:42) at file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/array.js:82:31 at Array.forEach () at unpack (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/array.js:61:12) at TupleCoder.decode (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/coders/tuple.js:60:16) at AbiCoder.decode (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/abi-coder.js:180:22) at Interface.decodeEventLog (file:///C:/guardian/guardian-service/node_modules/ethers/lib.esm/abi/interface.js:988:49) at syncWipeContract (file:///C:/guardian/guardian-service/dist/api/contract.service.js:328:35) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) at async Object.syncWipeContracts (file:///C:/guardian/guardian-service/dist/api/contract.service.js:298:9) at async CronJob.taskExecution (file:///C:/guardian/guardian-service/dist/helpers/synchronization-task.js:56:29)

Hi, again sorry for the delay.

Fixed the BUFFER_OVERRUN error. The root cause was a mismatch between the Solidity contract ABI and the TypeScript event ABI in contract.service.ts:

  • ABI fix (contract.service.ts): The Access.sol events use indexed parameters (event OwnerAdded(address indexed account)), but the TS ABI had event OwnerAdded(address) without indexed. This meant ethers tried to decode the address from log.data (empty) instead of log.topics. Fixed by adding indexed to the ABI and passing log.topics to all decodeEventLog calls.
  • Gas limits (contract-api.ts): The new ownership functions increased contract bytecode size, causing INSUFFICIENT_GAS on deployment. Updated defaults based on actual testnet measurements:
    • RETIRE_SINGLE_CONTRACT_GAS: 6M -> 7M (actual usage: 6.3M)
    • RETIRE_DOUBLE_CONTRACT_GAS: 7M -> 8M (actual usage: 7.3M)

@kirill-tolochko
Copy link
Copy Markdown

kirill-tolochko commented Feb 20, 2026

Hi @nosid91

I checked your fix. Now wipe and retire contracts are created, also tokens can be retired using these contracts.
But permissions update still doesn't work.

Steps to reproduce:
1. First two steps are the same, you don't need them if you already use freshly deployed contracts: deploy new Wipe, Retire, RetireSingleToken, RetireDoubleToken
2. Update contracts ids in the .env.guardian
3. Remove @usecache() in 269 row for @get('/:contractId/permissions') in api-gateway\src\api\service\contract.ts , rebuild api-gateway
4. Create wipe contract for your SR
5. Create new SR
6. Import created wipe contract for the second SR by clicking Import button. Put there hedera contract id and any description https://i.imgur.com/4adwjJz.png
7. Return to the first SR, click on three dots in Operations column in contracts, click Add admin in the dropdown and put HederaId of second SR https://i.imgur.com/CgID3uP.png
8. Return to the second SR, click on permissions update for imported contract https://i.imgur.com/vgqcVcq.png

Actual: nothing happened

Expected: green "Admin" badge should appear in the Permissions column for this imported policy

Also I have some questions and suggestions about changing the role feature:
1. Maybe we should put here also proposed/claimed/removed owner accountId? something like this

console.log(Owner ${newOwnerAddress} proposed for ${contractId}. Status: ${receipt.status.toString()});

2. Maybe it would be good to have console.log for unsuccessful try, but not sure.

3. Also it would be great to have normal error message in the hashscan if it is possible. now it is just https://imgur.com/HN13KDt
0x0e7c0580
Not sure that regular user undestands that it is "NoPermissions()"sliced error

4. And last but most important IMO: we need to somehow add the contract in the wipe/retire contracts list of the SR, if it was proposed and claimed by new SR. Now it is impossible to work with proposed and claimed contract in UI. Or maybe I just don't know how to properly get it for new user.

@nosid91
Copy link
Copy Markdown
Contributor Author

nosid91 commented Feb 20, 2026

Hi @nosid91

I checked your fix. Now wipe and retire contracts are created, also tokens can be retired using these contracts. But permissions update still doesn't work.

Steps to reproduce: 1. First two steps are the same, you don't need them if you already use freshly deployed contracts: deploy new Wipe, Retire, RetireSingleToken, RetireDoubleToken 2. Update contracts ids in the .env.guardian 3. Remove @usecache() in 269 row for @get('/:contractId/permissions') in api-gateway\src\api\service\contract.ts , rebuild api-gateway 4. Create wipe contract for your SR 5. Create new SR 6. Import created wipe contract for the second SR by clicking Import button. Put there hedera contract id and any description https://i.imgur.com/4adwjJz.png 7. Return to the first SR, click on three dots in Operations column in contracts, click Add admin in the dropdown and put HederaId of second SR https://i.imgur.com/CgID3uP.png 8. Return to the second SR, click on permissions update for imported contract https://i.imgur.com/vgqcVcq.png

Actual: nothing happened

Expected: green "Admin" badge should appear in the Permissions column for this imported policy

Also I have some questions and suggestions about changing the role feature: 1. Maybe we should put here also proposed/claimed/removed owner accountId? something like this

console.log(Owner ${newOwnerAddress} proposed for ${contractId}. Status: ${receipt.status.toString()});

2. Maybe it would be good to have console.log for unsuccessful try, but not sure.

3. Also it would be great to have normal error message in the hashscan if it is possible. now it is just https://imgur.com/HN13KDt 0x0e7c0580 Not sure that regular user undestands that it is "NoPermissions()"sliced error

4. And last but most important IMO: we need to somehow add the contract in the wipe/retire contracts list of the SR, if it was proposed and claimed by new SR. Now it is impossible to work with proposed and claimed contract in UI. Or maybe I just don't know how to properly get it for new user.

Hi! Thanks for the thorough review and the detailed reproduction steps.

Points 1-2: Agreed, I'll add the account addresses to all console.log messages and improve error output for failed attempts.
Point 3: Good catch. I'll replace custom errors with require string messages so they're human-readable in Hashscan instead of showing raw selectors like 0x0e7c0580. (This will slightly increase gas costs on reverts, but nothing critical.)
Point 4: This is a valid concern. However, looking at the original issue scope (#4055), the requirement was specifically about adding a method to change the OWNER role in smart contracts - which is now implemented via the two-step propose/claim pattern. The UI-side integration (auto-adding claimed contracts to the new SR's contract list) goes beyond what was specified in the original requirements. That said, it's a logical improvement — I'll need some time to figure out the best approach and implement it.
I will try to update the PR as soon as possible.

@nosid91 nosid91 force-pushed the dimaredchyts/feat-4055-Lack-of-Method-to-Change-OWNER-Role branch from 18eec91 to 6febcf6 Compare February 20, 2026 13:10
Replace custom NoPermissions() errors with require string messages
for human-readable output in Hashscan. Add account addresses to
CLI log messages for propose/claim/remove owner commands.

Signed-off-by: dima <nosid91@gmail.com>
…ords

Add OwnerAdded, OwnerRemoved, OwnerProposed event handlers to both
syncWipeContract and syncRetireContract. On OwnerAdded, automatically
create a contract DB record for the new owner if one doesn't exist.
On OwnerRemoved, reset permissions to 0. Send notifications for all
ownership events. Increase default Wipe contract deploy gas to 4M.

Signed-off-by: dima <nosid91@gmail.com>
@nosid91 nosid91 force-pushed the dimaredchyts/feat-4055-Lack-of-Method-to-Change-OWNER-Role branch from 6febcf6 to 696f688 Compare February 20, 2026 13:11
@nosid91
Copy link
Copy Markdown
Contributor Author

nosid91 commented Feb 20, 2026

@kirill-tolochko
managed to add this with minimal changes, just the sync handlers + error messages
Contract errors:

  • removed custom NoPermissions() error, replaced with require string messages
  • hashscan now shows readable text like "Access: caller is not the pending owner" instead of 0x0e7c0580
  • bumped default gas for wipe contract deployment from 2M to 4M since bytecoe got a bit larger with the string messages

CLI logs:

  • added account addresses to propose/claim/remove output
  • errors now include context instead of just a raw stack trace

Ownership event sync:

  • added OwnerAdded, OwnerRemoved, OwnerProposed handling in both sync functions (wipe + retire)
  • on OwnerAdded - automatically creates a contract DB record for the new owner if their account exists in guardian
  • on OwnerRemoved - resets permissions to 0 for the removed owner
  • on OwnerProposed - sends a notification

How to test

Case 1 - new owner exists in guardian:

  • create a wipe contract from SR-1
  • run propose-owner + claim-owner via cli for SR-2
  • wait for sync ~20-30 sec
  • contract should show up in SR-2's dashboard automatically

Case 2 - new owner is not in the db:

  • create a wipe contract from SR-1
  • propose + claim to an account thats not registered in guardian
  • sync doesnt crash, just skips creating the record
  • after registering the user with that account - import the contract manualy through UI

let me know if anything needs adjustments or if you'd like me to test any other edge cases. Thanks!

@kirill-tolochko
Copy link
Copy Markdown

@nosid91
Errors - ok ✅
Cli logs - ok ✅
Ownership - issues ❌

1. Now proposed user is able to manage contract - view requests, add/remove admins etc. It shouldn't happen until a proposal is claimed in my understanding. Or should be? Also user gets a badge "Owner" in the UI.
2. Remove-owner command doesn't remove owner anymore. User that was removed as owner is still able to manage the contract and has a badge "Owner" in the UI.

Just in case if you isn't able to reproduce, you can try my contracts
RETIRE_CONTRACT_FILE_ID="0.0.7990842"
WIPE_CONTRACT_FILE_ID="0.0.7990840"
RETIRE_SINGLE_FILE_ID="0.0.7990844"
RETIRE_DOUBLE_FILE_ID="0.0.7990846"

@kirill-tolochko
Copy link
Copy Markdown

Btw I forgot to mention that I also had to change } from '@hashgraph/sdk'; to } from '@hiero-ledger/sdk'; in guardian-cli/helpers/topic.helper.ts
and update solc version to "0.8.28" in guardian-cli/package.json

Does it work to you without changing?

@nosid91
Copy link
Copy Markdown
Contributor Author

nosid91 commented Feb 20, 2026

Btw I forgot to mention that I also had to change } from '@hashgraph/sdk'; to } from '@hiero-ledger/sdk'; in guardian-cli/helpers/topic.helper.ts and update solc version to "0.8.28" in guardian-cli/package.json

Does it work to you without changing?

yeah it works on my end, but only because the ownership commands (propose/claim/remove) go through contract.helper.ts which already uses @hiero-ledger/sdk. i didn't touch any topic commands during testing so the old import in topic.helper.ts wasn't hit. same with solc - i compiled contracts with foundry, not the npm package. so technically it builds and runs fine for my use case

…ecord on removal

Signed-off-by: dima <nosid91@gmail.com>
@nosid91
Copy link
Copy Markdown
Contributor Author

nosid91 commented Feb 20, 2026

@nosid91 Errors - ok ✅ Cli logs - ok ✅ Ownership - issues ❌

1. Now proposed user is able to manage contract - view requests, add/remove admins etc. It shouldn't happen until a proposal is claimed in my understanding. Or should be? Also user gets a badge "Owner" in the UI. 2. Remove-owner command doesn't remove owner anymore. User that was removed as owner is still able to manage the contract and has a badge "Owner" in the UI.

Just in case if you isn't able to reproduce, you can try my contracts RETIRE_CONTRACT_FILE_ID="0.0.7990842" WIPE_CONTRACT_FILE_ID="0.0.7990840" RETIRE_SINGLE_FILE_ID="0.0.7990844" RETIRE_DOUBLE_FILE_ID="0.0.7990846"

image fixed both, was a service-level issue not smart contract
  • OwnerAdded was copying all permissions from source (Owner+Admin+Manager), now sets only Owner
  • OwnerRemoved was just zeroing permissions but keeping the DB record, contract still showed up. now it deletes the record entirely

tested: propose without claim - only notification, no contract in dashboard (screenshot attached). claim - Owner badge only. remove - contract gone

@nosid91
Copy link
Copy Markdown
Contributor Author

nosid91 commented Feb 20, 2026

@kirill-tolochko
also updated what you mentioned - dc39674

  • changed @hashgraph/sdk → @hiero-ledger/sdk in topic.helper.ts
  • bumped solc from 0.8.26 to 0.8.28 in guardian-cli/package.json

@kirill-tolochko
Copy link
Copy Markdown

Hi @nosid91
Checked the fixes, looks like remove-owner command doesn't work on smart-contract level in some cases. You cannot remove ownership from the creator of the contract. You can delete the contract from DB, but if creator decides to re-import this contract, he still has all permissions including ownership.

But remove-owner command works correctly for any else owner who isn't creator of the contract.

Looks like it's impossible to remove any permission from the creator such as ownership/admin/manager roles?

@nosid91
Copy link
Copy Markdown
Contributor Author

nosid91 commented Feb 25, 2026

Hi @nosid91 Checked the fixes, looks like remove-owner command doesn't work on smart-contract level in some cases. You cannot remove ownership from the creator of the contract. You can delete the contract from DB, but if creator decides to re-import this contract, he still has all permissions including ownership.

But remove-owner command works correctly for any else owner who isn't creator of the contract.

Looks like it's impossible to remove any permission from the creator such as ownership/admin/manager roles?

hey, just to clarify - removeOwner does work on the smart contract level for the creator too. _unsetRole(account, OWNER) sets roles[OWNER][creator] = false, and after that permissions() returns 0 for the owner role. we tested this and it works
the thing you're describing - creator re-importing and still having permissions - thats because the constructor also grants ADMIN and MANAGER roles (_setRole(msg.sender, ADMIN), _setRole(msg.sender, MANAGER) in Wipe.sol). so even after removing OWNER, they still have those roles on-chain, and re-import picks them up via permissions()
but stripping all roles from the creator on removal would require changes to the smart contract logic itself, which goes beyond the original scope of #4055. the task was about adding the ability to change the OWNER role - propose, claim, remove - and thats implemented and working
could you detail the expected behavior for this case more precisely? like should removeOwner also revoke admin/manager? should re-import be blocked for removed owners? i think it would help to have clear acceptance criteria for these edge cases, ideally as a follow-up issue

@kirill-tolochko
Copy link
Copy Markdown

Please, take a look here https://hashscan.io/testnet/contract/0.0.8030982/calls
I removed owner that created a contract https://hashscan.io/testnet/transaction/1772016715.681985710/result (0.0.6046379)
And then I was able to propose new owner by former owner (0.0.6046379) that supposed to be removed. https://hashscan.io/testnet/transaction/1772016772.456567751/result
image
Are we sure that removeOwner works for creator?

@kirill-tolochko
Copy link
Copy Markdown

But it works correcty, if you remove new owner (that wasn't a creator) and then try to propose-owner by this removed new owner https://hashscan.io/testnet/transaction/1772017129.941019030/result

…patibility

Signed-off-by: dima <nosid91@gmail.com>
@nosid91
Copy link
Copy Markdown
Contributor Author

nosid91 commented Feb 25, 2026

Please, take a look here https://hashscan.io/testnet/contract/0.0.8030982/calls I removed owner that created a contract https://hashscan.io/testnet/transaction/1772016715.681985710/result (0.0.6046379) And then I was able to propose new owner by former owner (0.0.6046379) that supposed to be removed. https://hashscan.io/testnet/transaction/1772016772.456567751/result image Are we sure that removeOwner works for creator?

damn...
found it - ECDSA accounts on Hedera have two EVM addresses (long-zero from account ID and alias from the public key), constructor sets msg.sender as the alias but CLI was passing long-zero to removeOwner so it removed the role from the wrong address in the mapping. was testing with ED25519 which only has long-zero so it worked fine on my end. fixed in 30c08f2 - proposeOwner/removeOwner now resolve the actual evm_address via mirror node API instead of AccountId.toEvmAddress(), also added auto-detection for private key formats (ED25519 DER, ECDSA DER, ECDSA raw hex) so both key types work out of the box. verified with an ECDSA account: full cycle works, removed account gets CONTRACT_REVERT_EXECUTED when trying to call owner functions.

node dist/index.js propose-owner 0.0.7992552 0.0.7974131 0.0.7235511 <key>
https://hashscan.io/testnet/transaction/1772048025.534646771

node dist/index.js claim-owner 0.0.7992552 0.0.7974131 <key>
https://hashscan.io/testnet/transaction/1772048064.768184000

node dist/index.js remove-owner 0.0.7992552 0.0.7974131 0.0.7235511 <key>
https://hashscan.io/testnet/transaction/1772048082.644963339

node dist/index.js propose-owner 0.0.7992552 0.0.7235511 0.0.7974131 <key>
https://hashscan.io/testnet/transaction/1772048100.850203037 - CONTRACT_REVERT_EXECUTED

@kirill-tolochko
Copy link
Copy Markdown

kirill-tolochko commented Feb 26, 2026

Now propose-owner works as expected for user who has already been removed from ownership!

But..
Don't we need to make some changes in the guardian-service\src\api\contract.service.ts too?
I guess for example this one

case 'OwnerRemoved': {
                    const removedOwnerAccount = AccountId.fromEvmAddress(
                        0, 0, data[0]
                    ).toString();

won't work properly for ECDSA accounts, isn't it?

Instead of getting 0.0.6046379 we get something like 0.0.00f5613498ca93ecb326dccc4e3b8bf81814c960 and removedOwnerUser won't be found. So the contract won't be deleted from DB and user's contracts list.
The same for other 2 methods OwnerAdded and OwnerProposed. Contracts won't be added

@nosid91
Copy link
Copy Markdown
Contributor Author

nosid91 commented Feb 26, 2026

@kirill-tolochko good catch, same issue on the service side - AccountId.fromEvmAddress(0, 0, data[0]) produces a synthetic account ID for ECDSA aliases so getUserByAccount can't find the user. fixed in 40b44df- added resolveAccountFromEvmAddress() helper that resolves EVM addresses to real Hedera account IDs via mirror node API (Environment.HEDERA_ACCOUNT_API), replaced all 6 occurrences in OwnerAdded/OwnerRemoved/OwnerProposed handlers for both wipe and retire sync. tested with both ED25519 and ECDSA accounts - sync correctly creates/deletes contract records for both key types.

@nosid91 nosid91 force-pushed the dimaredchyts/feat-4055-Lack-of-Method-to-Change-OWNER-Role branch from a05fba2 to 40b44df Compare February 26, 2026 12:16
@kirill-tolochko
Copy link
Copy Markdown

Now it's ok and can be merged! @Pyatakov

@Pyatakov Pyatakov merged commit 78d1301 into hashgraph:develop Feb 27, 2026
12 checks passed
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.

5 participants