Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 14 additions & 20 deletions solidity/ecdsa/contracts/WalletRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -450,36 +450,30 @@ contract WalletRegistry is
}

/// @notice Withdraws application rewards for the given staking provider.
/// Rewards are withdrawn to the staking provider's beneficiary
/// address set in the staking contract. Reverts if staking provider
/// has not registered the operator address.
/// Rewards are sent to the beneficiary returned by
/// `_currentAuthorizationSource().rolesOf(stakingProvider)` — the
/// same authorization source used elsewhere after TIP-092 (Allowlist
/// when `allowlist != address(0)`, otherwise TokenStaking). Reverts
/// if the staking provider has not registered an operator address.
/// @dev Emits `RewardsWithdrawn` event.
///
/// NOT MIGRATED: Beneficiary lookup remains on TokenStaking because
/// migrating dead code costs 50-100 bytes with zero benefit.
/// Allowlist vs TokenStaking: `Allowlist.rolesOf` follows TIP-092 semantics
/// (beneficiary is the staking provider address), while `TokenStaking.rolesOf`
/// can return a distinct delegated beneficiary. After `initializeV2(allowlist)`,
/// reward payout follows Allowlist, not legacy TokenStaking role resolution.
///
/// Historical Context (TIP-092/100 - February 15, 2025):
/// - Sortition pool DKG participation rewards HALTED Feb 15, 2025
/// - TokenStaking notification rewards HALTED for ECDSA/RandomBeacon
/// - Only TACo application rewards continue (6-month transition)
/// - This function now returns 0 for all ECDSA operators (no rewards)
///
/// Migration Decision Rationale:
/// - Bytecode cost: 50-100 bytes to migrate beneficiary lookup to Allowlist
/// - Benefit: Zero (function returns 0 - no rewards to withdraw)
/// - Preserved for historical compatibility and potential future reactivation
///
/// Technical Note: If rewards are reactivated, Allowlist migration would
/// be required as Allowlist.rolesOf() always returns stakingProvider as
/// beneficiary (no delegation support), while TokenStaking.rolesOf()
/// returns configured beneficiary (supports owner != beneficiary delegation).
///
/// Stakeholder Decision: Pragmatic choice to avoid bytecode cost for
/// dead code, predating TIP-092/100 implementation.
/// - For ECDSA, expect zero withdrawable amount unless application rewards
/// are reactivated.
function withdrawRewards(address stakingProvider) external {
address operator = stakingProviderToOperator(stakingProvider);
if (operator == address(0)) revert UnknownOperator();
(, address beneficiary, ) = staking.rolesOf(stakingProvider);
(, address beneficiary, ) = _currentAuthorizationSource().rolesOf(
stakingProvider
);
uint96 amount = sortitionPool.withdrawRewards(operator, beneficiary);
// slither-disable-next-line reentrancy-events
emit RewardsWithdrawn(stakingProvider, amount);
Expand Down
67 changes: 66 additions & 1 deletion solidity/ecdsa/test/WalletRegistry.Rewards.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { helpers } from "hardhat"
import { ethers, helpers } from "hardhat"
import { expect } from "chai"

import { walletRegistryFixture } from "./fixtures"
Expand All @@ -10,6 +10,7 @@ import type { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"
import type { FakeContract } from "@defi-wonderland/smock"
import type { Operator, OperatorID } from "./utils/operators"
import type {
Allowlist,
SortitionPool,
WalletRegistry,
WalletRegistryGovernance,
Expand Down Expand Up @@ -243,4 +244,68 @@ describe("WalletRegistry - Rewards", () => {
})
})
})

describe("withdrawRewards when allowlist != address(0)", () => {
const walletPublicKeyLocal: string = ecdsaData.group1.publicKey

let tTokenLocal: T
let walletRegistryLocal: WalletRegistry
let sortitionPoolLocal: SortitionPool
let randomBeaconLocal: FakeContract<IRandomBeacon>
let walletOwnerLocal: FakeContract<IWalletOwner>
let deployerLocal: SignerWithAddress
let membersLocal: Operator[]

before(async () => {
await createSnapshot()
;({
tToken: tTokenLocal,
walletRegistry: walletRegistryLocal,
sortitionPool: sortitionPoolLocal,
randomBeacon: randomBeaconLocal,
walletOwner: walletOwnerLocal,
deployer: deployerLocal,
} = await walletRegistryFixture({ useAllowlist: true }))
expect(await walletRegistryLocal.allowlist()).to.not.equal(
ethers.constants.AddressZero
)
;({ members: membersLocal } = await createNewWallet(
walletRegistryLocal,
walletOwnerLocal.wallet,
randomBeaconLocal,
walletPublicKeyLocal
))
})

after(async () => {
await restoreSnapshot()
})

it("should pay rewards to _currentAuthorizationSource().rolesOf beneficiary (Allowlist)", async () => {
const operator = membersLocal[0].signer.address
const stakingProvider =
await walletRegistryLocal.operatorToStakingProvider(operator)
const allowlistAddr = await walletRegistryLocal.allowlist()
const allowlist = (await ethers.getContractAt(
"Allowlist",
allowlistAddr
)) as Allowlist

const { beneficiary: expectedBeneficiary } = await allowlist.rolesOf(
stakingProvider
)
expect(expectedBeneficiary).to.equal(stakingProvider)

await tTokenLocal
.connect(deployerLocal)
.mint(deployerLocal.address, rewardAmount)
await tTokenLocal
.connect(deployerLocal)
.approveAndCall(sortitionPoolLocal.address, rewardAmount, [])

expect(await tTokenLocal.balanceOf(expectedBeneficiary)).to.equal(0)
await walletRegistryLocal.withdrawRewards(stakingProvider)
expect(await tTokenLocal.balanceOf(expectedBeneficiary)).to.be.gt(0)
})
})
})
Loading