Skip to content

Commit 55e0871

Browse files
pahor167Pavel Hornak
andauthored
fix: prevent stuck withdrawals when Election cap exhausts a group (#235)
* fix: prevent and recover stuck withdrawals (Election cap exhaustion) Rebased onto releases/4 for release. Root cause: Account.scheduleWithdrawals validated per-group capacity via getCeloForGroup, which counts scheduledVotes.toVote even when the Election numVotesReceivable cap permanently blocks it. Withdrawals scheduled against that stale toVote could never be revoked, pinning a user's CELO to a group with no recovery path (forum.celo.org/t/.../13333). Fix: - New Account.getRealisableCeloForGroup = max(0, revokable - toRevoke) + min(balance, toVote), minus existing toWithdraw once (withdraw is immediate-first, so a pending withdrawal costs one unit of combined capacity, not one per bucket). - scheduleWithdrawals books each entry through _addBeneficiaryWithdrawal, which validates against realisable and threads a shared balance budget so a multi-group call cannot double-count the same unlocked CELO. - DefaultStrategy / SpecificGroupStrategy withdrawal distribution cap each group by realisable (real withdrawals) and skip zero-capacity / already chosen groups, so distribution never pins to a group that cannot pay and never emits a group twice. Transfers (isTransfer) stay accounting-only. - New permissionless Account.rescueScheduledWithdrawal re-routes an existing stuck pin to groups that can fulfill it, gated by _requireGroupInDeficit so it only fires when the group genuinely cannot pay the beneficiary now. Versions bumped per the contract-compatibility check against releases/4: Account 1.2.2.0 (minor: added view + rescue), DefaultStrategy 1.3.0.0 (major: generateWithdrawalVoteDistribution gained isTransfer), SpecificGroupStrategy 1.1.1.1 and Manager 1.3.1.1 (patch). Tests: account-realisable.test.ts (realisable + single-subtraction + rescue), end-to-end-stuck-withdrawal.test.ts (real-contract prevention, distribution, no-duplicate-group, rescue gating, shared balance budget), plus scripts/verify-stuck-withdrawal-mainnet-fork.sh (anvil mainnet-fork proof). Full suite green; compatibility check passes against releases/4. * ci: run CI-Pipeline for release branches (releases/**), not just master PR 235 targets releases/4; the workflow only triggered on PRs into master, so retargeting left it without CI. Add releases/** to the push and pull_request branch filters so release-targeted PRs get lint, test, and the contract-compatibility check. * fix: align contract versions with releases/4 compatibility baseline The contract-compatibility CI job compares against a baseline one increment below releases/4 source and requires getVersionNumber to land exactly on the releases/4 source values (Account 1.2.1.0, DefaultStrategy 1.2.0.0, SpecificGroupStrategy 1.1.1.0, Manager 1.3.1.0). An earlier local run read the baseline one step higher (different toolchain) and over-bumped; revert to the CI-mandated values. * chore: add governance release script for stuck-withdrawal fix Releases the 4 upgraded stCELO impls (Account, DefaultStrategy, SpecificGroupStrategy, Manager) via a Celo Governance proposal. The proxies are owned by the stCELO MultiSig, which exposes governanceProposeAndExecute (onlyGovernance) - so a single CGP tx (Governance -> MultiSig governanceProposeAndExecute([4 proxies],[0,0,0,0],[upgradeTo x4])) performs all four upgrades, no owner timelock. The script deploys the impls, builds the CGP calldata + JSON, and (default fork-test mode) impersonates Celo Governance to execute it and assert: all 4 proxies repointed, pre-existing storage intact, new code live, versions at release values. Storage layout verified unchanged vs releases/4 (solc layout diff + CI compat + on-fork state-intact). --emit-only prints the CGP without executing. * chore: release script supports fork + mainnet, with explorer verification - 'fork' (default): unchanged - fork mainnet, deploy ephemeral impls, impersonate Celo Governance, execute governanceProposeAndExecute, assert upgrades + storage intact + new code live. - 'mainnet': real broadcast (needs DEPLOYER_PK, CELO_SCAN_API_KEY). Deploys the 4 impls + AddressSortedLinkedList library, verifies each on Celoscan (hardhat verify) AND Blockscout (forge verify-contract --verifier blockscout), then builds + prints the CGP tx/JSON with the real addresses. Does not execute the upgrade (Governance does, on vote pass). DefaultStrategy verification passes the library link on both verifiers. solc 0.8.11 / evm istanbul / optimizer disabled (matches hardhat.config). * fix: correct DefaultStrategy library link + surface deploy errors The library-link step reused norm() (strips leading zeros) to substitute the deployed AddressSortedLinkedList address into DefaultStrategy's bytecode placeholder. If the library address has a leading zero this produces a wrong-length (malformed) link and the deploy fails - which is what happened on the mainnet attempt. Preserve the full 40-hex address (0x stripped, lowercased, leading zeros kept) and assert no unlinked placeholder remains. Also: deploy() no longer swallows cast stderr - it prints the real error and aborts, so a failed deploy (bad link, gas, or insufficient funds) is diagnosable instead of an empty address. * fix: explicit incrementing nonce for sequential deploys forno's 'latest' nonce can lag the sequencer, so cast auto-nonce submitted a stale value (nonce too low: next 28, tx 27). Anchor to the PENDING nonce once and pass an explicit, caller-incremented --nonce for each of the 5 deploys so they stay in lock-step with the sequencer. Nonce is incremented in the caller, not inside the deploy function (it runs in a command-substitution subshell). * fix: verify Celoscan via forge (avoid hardhat-etherscan solc-bin DNS dep) hardhat verify failed with 'getaddrinfo ENOTFOUND solc-bin.ethereum.org' - the @nomiclabs/hardhat-etherscan plugin fetches the solc version list from that host before verifying. Switch the Celoscan step to forge verify-contract --chain celo (same as Blockscout, which passed), which uses the local solc and has no solc-bin dependency. Both explorers now verified through forge. --------- Co-authored-by: Pavel Hornak <pavel.hornak@clabs.co>
1 parent 04c18ac commit 55e0871

14 files changed

Lines changed: 2409 additions & 27 deletions

.github/workflows/solidity.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
name: CI-Pipeline
22

33
on:
4-
# Triggers the workflow on push or pull request events but only for the main branch
4+
# Triggers on push or pull request events for master and release branches.
55
push:
6-
branches: [master]
6+
branches: [master, "releases/**"]
77
pull_request:
8-
branches: [master]
8+
branches: [master, "releases/**"]
99

1010
# Allows you to run this workflow manually from the Actions tab
1111
workflow_dispatch:

contracts/Account.sol

Lines changed: 165 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,17 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
187187
/// @notice There's no amount of scheduled withdrawal for the given beneficiary and group.
188188
error NoScheduledWithdrawal(address beneficiary, address group);
189189

190+
/**
191+
* @notice Thrown when `rescueScheduledWithdrawal` targets a group that can
192+
* still pay the beneficiary's withdrawal - so no rescue is warranted.
193+
*/
194+
error GroupNotInDeficit(
195+
address beneficiary,
196+
address group,
197+
uint256 capacity,
198+
uint256 userClaim
199+
);
200+
190201
/// @notice Voting for proposal was not successfull.
191202
error VotingNotSuccessful(uint256 proposalId);
192203

@@ -325,19 +336,19 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
325336
revert GroupsAndVotesArrayLengthsMismatch();
326337
}
327338

339+
// Thread one balance budget across groups: balance is shared, so
340+
// validating each group independently would double-count the same CELO.
341+
uint256 balanceBudget = address(this).balance;
328342
uint256 totalWithdrawalsDelta;
329343

330344
for (uint256 i = 0; i < withdrawals.length; i++) {
331-
uint256 celoAvailableForGroup = getCeloForGroup(groups[i]);
332-
if (celoAvailableForGroup < withdrawals[i]) {
333-
revert WithdrawalAmountTooHigh(groups[i], celoAvailableForGroup, withdrawals[i]);
334-
}
335-
336-
scheduledVotes[groups[i]].toWithdraw += withdrawals[i];
337-
scheduledVotes[groups[i]].toWithdrawFor[beneficiary] += withdrawals[i];
345+
balanceBudget = _addBeneficiaryWithdrawal(
346+
beneficiary,
347+
groups[i],
348+
withdrawals[i],
349+
balanceBudget
350+
);
338351
totalWithdrawalsDelta += withdrawals[i];
339-
340-
emit CeloWithdrawalScheduled(beneficiary, groups[i], withdrawals[i]);
341352
}
342353

343354
totalScheduledWithdrawals += totalWithdrawalsDelta;
@@ -580,6 +591,60 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
580591
}
581592
}
582593

594+
/**
595+
* @notice Re-routes a beneficiary's pinned withdrawal onto groups that can
596+
* fulfill it, freeing CELO stuck on a group whose Election votes fell below
597+
* the pinned amount. Bookkeeping only; no CELO moves. Permissionless, but
598+
* only when `fromGroup` truly cannot pay this beneficiary right now (see
599+
* `_requireGroupInDeficit`). Funds can only ever reach `beneficiary` via
600+
* `Account.withdraw`, so a caller can at worst route to slower groups -
601+
* never steal or destroy CELO.
602+
* @dev Net change to `totalScheduledWithdrawals` is zero, so it isn't
603+
* touched. The source slot is cleared before the additions, so passing
604+
* `fromGroup` inside `toGroups` re-adds onto a clean slot instead of
605+
* zeroing the beneficiary's claim.
606+
* @param beneficiary The beneficiary of the withdrawal.
607+
* @param fromGroup The group the withdrawal is currently pinned to.
608+
* @param toGroups Groups to re-route the withdrawal across.
609+
* @param amounts Per-group CELO amounts. Sum must equal the pinned amount.
610+
*/
611+
function rescueScheduledWithdrawal(
612+
address beneficiary,
613+
address fromGroup,
614+
address[] calldata toGroups,
615+
uint256[] calldata amounts
616+
) external onlyWhenNotPaused {
617+
if (toGroups.length != amounts.length) {
618+
revert GroupsAndVotesArrayLengthsMismatch();
619+
}
620+
uint256 original = scheduledVotes[fromGroup].toWithdrawFor[beneficiary];
621+
if (original == 0) {
622+
revert NoScheduledWithdrawal(beneficiary, fromGroup);
623+
}
624+
_requireGroupInDeficit(beneficiary, fromGroup, original);
625+
626+
// Clear source FIRST so a malformed `toGroups` containing
627+
// `fromGroup` cannot trigger an "add-then-zero" sequence that
628+
// would destroy the beneficiary's claim.
629+
scheduledVotes[fromGroup].toWithdrawFor[beneficiary] = 0;
630+
scheduledVotes[fromGroup].toWithdraw -= original;
631+
632+
uint256 balanceBudget = address(this).balance;
633+
uint256 sum;
634+
for (uint256 i = 0; i < toGroups.length; i++) {
635+
balanceBudget = _addBeneficiaryWithdrawal(
636+
beneficiary,
637+
toGroups[i],
638+
amounts[i],
639+
balanceBudget
640+
);
641+
sum += amounts[i];
642+
}
643+
if (sum != original) {
644+
revert TransferAmountMisalignment(original, sum);
645+
}
646+
}
647+
583648
/**
584649
* @notice Gets the total amount of CELO this contract controls. This is the
585650
* unlocked CELO balance of the contract plus the amount of LockedGold for this contract,
@@ -762,6 +827,9 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
762827
* @notice Returns the total amount of CELO directed towards `group`. This is
763828
* the Unlocked CELO balance for `group` plus the combined amount in pending
764829
* and active votes made by this contract.
830+
* @dev WARNING: includes `scheduledVotes.toVote` even when Election cap is
831+
* exhausted and those votes can never be cast. For withdrawal-capacity
832+
* decisions use `getRealisableCeloForGroup` instead.
765833
* @param group The address of the validator group.
766834
* @return The total amount of CELO directed towards `group`.
767835
*/
@@ -777,6 +845,29 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
777845
return 0;
778846
}
779847

848+
/**
849+
* @notice CELO for `group` that `Account.withdraw` can actually pay out now,
850+
* unlike `getCeloForGroup` which counts `toVote` the Election cap may block.
851+
* @dev Two buckets: revokable Election votes, and the `toVote` slice backed
852+
* by current balance (withdraw pays that directly, no Election vote needed).
853+
* Subtract queued `toRevoke` from the revoke bucket, and existing
854+
* `toWithdraw` once from the total - withdraw is immediate-first, so a
855+
* pending withdrawal costs one unit of total capacity, not one per bucket.
856+
* @param group The address of the validator group.
857+
* @return The realisable CELO for `group`.
858+
*/
859+
function getRealisableCeloForGroup(address group) public view returns (uint256) {
860+
uint256 revokable = getElection().getTotalVotesForGroupByAccount(group, address(this));
861+
uint256 toRevoke = scheduledVotes[group].toRevoke;
862+
uint256 revokableAvail = revokable > toRevoke ? revokable - toRevoke : 0;
863+
uint256 toVote = scheduledVotes[group].toVote;
864+
uint256 balance = address(this).balance;
865+
uint256 immediateBucket = balance < toVote ? balance : toVote;
866+
uint256 capacity = revokableAvail + immediateBucket;
867+
uint256 toWithdraw = scheduledVotes[group].toWithdraw;
868+
return capacity > toWithdraw ? capacity - toWithdraw : 0;
869+
}
870+
780871
/**
781872
* @notice Returns the total amount of CELO that's voted with for a group.
782873
* @param group The address of the validator group.
@@ -946,4 +1037,69 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
9461037
emit RevocationScheduled(group, addToRevoke);
9471038
}
9481039
}
1040+
1041+
/**
1042+
* @notice Books a per-group withdrawal for `rescueScheduledWithdrawal`,
1043+
* reverting unless the group can realisably fulfill it. Returns the
1044+
* balance budget left after this pin's immediate draw.
1045+
*/
1046+
function _addBeneficiaryWithdrawal(
1047+
address beneficiary,
1048+
address toGroup,
1049+
uint256 amount,
1050+
uint256 balanceBudget
1051+
) private returns (uint256 newBalanceBudget) {
1052+
// Capacity = immediate bucket + revoke bucket, minus what existing
1053+
// pins on this group already claim (subtracted once - see
1054+
// getRealisableCeloForGroup). Charge the budget by this pin's immediate
1055+
// draw so a later group can't reuse the same balance.
1056+
uint256 immediate;
1057+
{
1058+
uint256 revokable = getElection().getTotalVotesForGroupByAccount(
1059+
toGroup,
1060+
address(this)
1061+
);
1062+
uint256 toRevoke = scheduledVotes[toGroup].toRevoke;
1063+
uint256 revokableAvail = revokable > toRevoke ? revokable - toRevoke : 0;
1064+
uint256 toVote = scheduledVotes[toGroup].toVote;
1065+
uint256 immediateBucket = balanceBudget < toVote ? balanceBudget : toVote;
1066+
uint256 capacity = revokableAvail + immediateBucket;
1067+
uint256 existingToWithdraw = scheduledVotes[toGroup].toWithdraw;
1068+
uint256 capacityForNew = capacity > existingToWithdraw
1069+
? capacity - existingToWithdraw
1070+
: 0;
1071+
if (amount > capacityForNew) {
1072+
revert WithdrawalAmountTooHigh(toGroup, capacityForNew, amount);
1073+
}
1074+
immediate = amount < immediateBucket ? amount : immediateBucket;
1075+
}
1076+
scheduledVotes[toGroup].toWithdraw += amount;
1077+
scheduledVotes[toGroup].toWithdrawFor[beneficiary] += amount;
1078+
emit CeloWithdrawalScheduled(beneficiary, toGroup, amount);
1079+
return balanceBudget - immediate;
1080+
}
1081+
1082+
/**
1083+
* @notice Reverts unless `group` genuinely cannot pay `userClaim` to this
1084+
* beneficiary now - the gate that keeps `rescueScheduledWithdrawal`
1085+
* permissionless without letting anyone reroute a payable withdrawal.
1086+
* @dev Capacity is what `Account.withdraw(beneficiary, group)` could deliver
1087+
* for THIS claim: `min(balance, toVote) + revokable`. It deliberately
1088+
* ignores `toRevoke` and other beneficiaries' `toWithdraw` - withdraw isn't
1089+
* gated by those, so counting them could mark a payable group as deficient.
1090+
*/
1091+
function _requireGroupInDeficit(
1092+
address beneficiary,
1093+
address group,
1094+
uint256 userClaim
1095+
) private view {
1096+
uint256 revokable = getElection().getTotalVotesForGroupByAccount(group, address(this));
1097+
uint256 toVote = scheduledVotes[group].toVote;
1098+
uint256 balance = address(this).balance;
1099+
uint256 immediate = toVote < balance ? toVote : balance;
1100+
uint256 capacity = immediate + revokable;
1101+
if (capacity >= userClaim) {
1102+
revert GroupNotInDeficit(beneficiary, group, capacity, userClaim);
1103+
}
1104+
}
9491105
}

contracts/DefaultStrategy.sol

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ contract DefaultStrategy is Errors, UUPSOwnableUpgradeable, Managed, Pausable {
412412
* @return finalGroups The groups that were chosen for subtraction.
413413
* @return finalVotes The votes of chosen finalGroups.
414414
*/
415-
function generateWithdrawalVoteDistribution(uint256 celoAmount)
415+
function generateWithdrawalVoteDistribution(uint256 celoAmount, bool isTransfer)
416416
external
417417
managerOrStrategy
418418
returns (address[] memory finalGroups, uint256[] memory finalVotes)
@@ -430,14 +430,22 @@ contract DefaultStrategy is Errors, UUPSOwnableUpgradeable, Managed, Pausable {
430430
uint256 groupsIndex;
431431

432432
while (groupsIndex < maxGroupCount && celoAmount != 0 && votedGroup != address(0)) {
433-
votes[groupsIndex] = Math.min(
434-
Math.min(
435-
account.getCeloForGroup(votedGroup),
436-
IManager(manager).toCelo(stCeloInGroup[votedGroup])
437-
),
438-
celoAmount
439-
);
433+
// Real withdrawals cap by realisable CELO so we never pin to a
434+
// group that can't pay. Transfers (isTransfer) are accounting-only
435+
// and the caller discards the votes, so they skip that cap.
436+
uint256 stCeloAsCelo = IManager(manager).toCelo(stCeloInGroup[votedGroup]);
437+
uint256 capacity = isTransfer
438+
? stCeloAsCelo
439+
: Math.min(account.getRealisableCeloForGroup(votedGroup), stCeloAsCelo);
440+
441+
if (capacity == 0) {
442+
// Nothing to take here; move to the next-smaller group without
443+
// using a slot. (Re-reading HEAD would loop on this group.)
444+
(, votedGroup, ) = activeGroups.get(votedGroup);
445+
continue;
446+
}
440447

448+
votes[groupsIndex] = Math.min(capacity, celoAmount);
441449
groups[groupsIndex] = votedGroup;
442450
celoAmount -= votes[groupsIndex];
443451
_updateGroupStCelo(
@@ -447,13 +455,31 @@ contract DefaultStrategy is Errors, UUPSOwnableUpgradeable, Managed, Pausable {
447455
);
448456
trySort(votedGroup, stCeloInGroup[votedGroup], false);
449457

458+
address previous = votedGroup;
450459
if (sorted) {
451460
votedGroup = activeGroups.getHead();
452461
} else {
453-
(, votedGroup, ) = activeGroups.get(votedGroup);
462+
(, votedGroup, ) = activeGroups.get(previous);
454463
}
455464

456465
groupsIndex++;
466+
467+
// Skip groups already chosen this call: when realisable (not
468+
// stCelo) is the binding cap, a group's stCelo barely drops and it
469+
// can resurface as HEAD, and emitting it twice would make
470+
// scheduleWithdrawals revert. Walking toward smaller groups never
471+
// skips an unchosen one (HEAD-down order).
472+
bool chosen = true;
473+
while (votedGroup != address(0) && chosen) {
474+
chosen = false;
475+
for (uint256 j = 0; j < groupsIndex; j++) {
476+
if (groups[j] == votedGroup) {
477+
chosen = true;
478+
(, votedGroup, ) = activeGroups.get(votedGroup);
479+
break;
480+
}
481+
}
482+
}
457483
}
458484

459485
if (celoAmount != 0) {

contracts/Manager.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,7 @@ contract Manager is Errors, UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Pa
671671
.generateWithdrawalVoteDistribution(strategy, celoAmount, stCeloAmount, isTransfer);
672672
} else {
673673
(groupsWithdrawn, withdrawalsPerGroup) = defaultStrategy
674-
.generateWithdrawalVoteDistribution(celoAmount);
674+
.generateWithdrawalVoteDistribution(celoAmount, isTransfer);
675675
}
676676

677677
return (groupsWithdrawn, withdrawalsPerGroup);

contracts/SpecificGroupStrategy.sol

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ contract SpecificGroupStrategy is Errors, UUPSOwnableUpgradeable, Managed, Pausa
279279
celoWithdrawalAmount -= celoToBeMovedFromDefaultStrategy;
280280

281281
(address[] memory overflowGroups, uint256[] memory overflowVotes) = defaultStrategy
282-
.generateWithdrawalVoteDistribution(celoToBeMovedFromDefaultStrategy);
282+
.generateWithdrawalVoteDistribution(celoToBeMovedFromDefaultStrategy, isTransfer);
283283

284284
handleWithdrawalOverflowAndUnhealthyAccounting(
285285
group,
@@ -294,7 +294,11 @@ contract SpecificGroupStrategy is Errors, UUPSOwnableUpgradeable, Managed, Pausa
294294
groups[i] = overflowGroups[i];
295295
votes[i] = overflowVotes[i];
296296
}
297-
if (!isTransfer && account.getCeloForGroup(group) < celoWithdrawalAmount) {
297+
if (
298+
!isTransfer && account.getRealisableCeloForGroup(group) < celoWithdrawalAmount
299+
) {
300+
// Use realisable (not getCeloForGroup) so we don't pin to a
301+
// group whose votes the Election cap won't let us revoke.
298302
revert GroupNotBalanced(group);
299303
}
300304
groups[overflowGroups.length] = group;
@@ -623,8 +627,9 @@ contract SpecificGroupStrategy is Errors, UUPSOwnableUpgradeable, Managed, Pausa
623627
*/
624628
function transferFromDefaultStrategy(address group, uint256 stCeloToMove) private {
625629
uint256 toMoveCelo = IManager(manager).toCelo(stCeloToMove);
630+
// isTransfer=true: accounting-only move, so DS skips the realisable cap.
626631
(address[] memory fromGroups, uint256[] memory fromVotes) = defaultStrategy
627-
.generateWithdrawalVoteDistribution(toMoveCelo);
632+
.generateWithdrawalVoteDistribution(toMoveCelo, true);
628633
address[] memory toGroups = new address[](1);
629634
uint256[] memory toVotes = new uint256[](1);
630635
toGroups[0] = group;

contracts/interfaces/IAccount.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ interface IAccount {
2929

3030
function getCeloForGroup(address) external view returns (uint256);
3131

32+
function getRealisableCeloForGroup(address group) external view returns (uint256);
33+
3234
function scheduledVotesForGroup(address group) external view returns (uint256);
3335

3436
function scheduledRevokeForGroup(address group) external view returns (uint256);

contracts/interfaces/IDefaultStrategy.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface IDefaultStrategy {
88
address depositGroupToIgnore
99
) external returns (address[] memory finalGroups, uint256[] memory finalVotes);
1010

11-
function generateWithdrawalVoteDistribution(uint256 celoAmount)
11+
function generateWithdrawalVoteDistribution(uint256 celoAmount, bool isTransfer)
1212
external
1313
returns (address[] memory finalGroups, uint256[] memory finalVotes);
1414

0 commit comments

Comments
 (0)