Platform: Code4rena
Start Date: 15/12/2022
Pot Size: $128,000 USDC
Total HM: 28
Participants: 111
Period: 19 days
Judge: GalloDaSballo
Total Solo HM: 1
Id: 194
League: ETH
Rank: 28/111
Findings: 6
Award: $790.62
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: gz627
Also found by: AkshaySrivastav, Allarious, Czar102, HE1M, HollaDieWaldfee, KmanOfficial, adriro, ast3ros, betweenETHlines, bin2chen, brgltd, cccz, chaduke, hihen, imare, mookimgo, neumo, nogo, peanuts, unforgiven
21.713 USDC - $21.71
In the ProtocolDAO.upgradeExistingContract function, registerContract is called first and then unregisterContract is called.
function upgradeExistingContract( address newAddr, string memory newName, address existingAddr ) external onlyGuardian { registerContract(newAddr, newName); unregisterContract(existingAddr); }
If newName == oldName when upgradeExistingContract is called, then in the registerContract function, the address corresponding to name will be set to newAddr first
function registerContract(address addr, string memory name) public onlyGuardian { setBool(keccak256(abi.encodePacked("contract.exists", addr)), true); setAddress(keccak256(abi.encodePacked("contract.address", name)), addr); setString(keccak256(abi.encodePacked("contract.name", addr)), name); }
and when deleteAddress is called in the unregisterContract function, the newAddr corresponding to name will be deleted, so that the address corresponding to name will be 0.
function unregisterContract(address addr) public onlyGuardian { string memory name = getContractName(addr); deleteBool(keccak256(abi.encodePacked("contract.exists", addr))); deleteAddress(keccak256(abi.encodePacked("contract.address", name))); deleteString(keccak256(abi.encodePacked("contract.name", addr))); }
Afterwards, it will be reverted in getContractAddress.
function getContractAddress(string memory contractName) internal view returns (address) { address contractAddress = getAddress(keccak256(abi.encodePacked("contract.address", contractName))); if (contractAddress == address(0x0)) { revert ContractNotFound(); } return contractAddress; }
https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/ProtocolDAO.sol#L209-L216 https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/ProtocolDAO.sol#L190-L203 https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/BaseAbstract.sol#L90-L96
None
Consider swapping the order of registerContract and unregisterContract calls in the upgradeExistingContract function
function upgradeExistingContract( address newAddr, string memory newName, address existingAddr ) external onlyGuardian { - registerContract(newAddr, newName); unregisterContract(existingAddr); + registerContract(newAddr, newName); }
#0 - c4-judge
2023-01-09T10:05:21Z
GalloDaSballo marked the issue as duplicate of #742
#1 - c4-judge
2023-02-08T20:09:50Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: __141345__
Also found by: 0xbepresent, Deivitto, HollaDieWaldfee, Nyx, PaludoX0, RaymondFam, ak1, cccz, ck, cryptostellar5, csanuragjain, ladboy233, rvierdiiev
68.0946 USDC - $68.09
The Staking.stakeGGP function has the whenNotPaused modifier, which means that when the contract is paused, it is not possible to stake the GGP with the stakeGGP function
function stakeGGP(uint256 amount) external whenNotPaused { // Transfer GGP tokens from staker to this contract ggp.safeTransferFrom(msg.sender, address(this), amount); _stakeGGP(msg.sender, amount); }
But the restakeGGP function does not have the whenNotPaused modifier, which means that the user can call ClaimNodeOp.claimAndRestake to stake GGP when the contract is paused
function restakeGGP(address stakerAddr, uint256 amount) public onlySpecificRegisteredContract("ClaimNodeOp", msg.sender) { // Transfer GGP tokens from the ClaimNodeOp contract to this contract ggp.safeTransferFrom(msg.sender, address(this), amount); _stakeGGP(stakerAddr, amount); } ... function claimAndRestake(uint256 claimAmt) external { Staking staking = Staking(getContractAddress("Staking")); uint256 ggpRewards = staking.getGGPRewards(msg.sender); if (ggpRewards == 0) { revert NoRewardsToClaim(); } if (claimAmt > ggpRewards) { revert InvalidAmount(); } staking.decreaseGGPRewards(msg.sender, ggpRewards); Vault vault = Vault(getContractAddress("Vault")); uint256 restakeAmt = ggpRewards - claimAmt; if (restakeAmt > 0) { vault.withdrawToken(address(this), ggp, restakeAmt); ggp.approve(address(staking), restakeAmt); staking.restakeGGP(msg.sender, restakeAmt); }
None
Consider adding the whenNotPaused modifier to the Staking.restakeGGP function
#0 - c4-judge
2023-01-08T13:28:04Z
GalloDaSballo marked the issue as duplicate of #351
#1 - c4-judge
2023-01-29T18:15:30Z
GalloDaSballo marked the issue as duplicate of #673
#2 - c4-judge
2023-02-08T08:56:52Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: hansfriese
Also found by: 0Kage, 0xdeadbeef0x, Allarious, Aymen0909, Ch_301, Franfran, HollaDieWaldfee, Jeiwan, Nyx, RaymondFam, SmartSek, adriro, betweenETHlines, bin2chen, cccz, datapunk, immeas, kaliberpoziomka8552, peritoflores, wagmi
21.713 USDC - $21.71
In requireValidStateTransition, the recreateMinipool function can be called to create a minipool when the current state is Withdrawable/Error/Finished/Canceled
} else if (currentStatus == MinipoolStatus.Withdrawable || currentStatus == MinipoolStatus.Error) { isValid = (to == MinipoolStatus.Finished || to == MinipoolStatus.Prelaunch); } else if (currentStatus == MinipoolStatus.Finished || currentStatus == MinipoolStatus.Canceled) { // Once a node is finished/canceled, if they re-validate they go back to beginning state isValid = (to == MinipoolStatus.Prelaunch); ... function recreateMinipool(address nodeID) external whenNotPaused { int256 minipoolIndex = onlyValidMultisig(nodeID); requireValidStateTransition(minipoolIndex, MinipoolStatus.Prelaunch);
The recreateMinipool function does not require the user to provide new avax, but uses the user's untaken avax. We also note that the withdrawMinipoolFunds function sets the status to Finished, which means that the recreateMinipool function can be called to create a minipool even if the remained avax is withdrawn
function withdrawMinipoolFunds(address nodeID) external nonReentrant { int256 minipoolIndex = requireValidMinipool(nodeID); address owner = onlyOwner(minipoolIndex); requireValidStateTransition(minipoolIndex, MinipoolStatus.Finished); setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Finished)); uint256 avaxNodeOpAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpAmt"))); uint256 avaxNodeOpRewardAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpRewardAmt"))); uint256 totalAvaxAmt = avaxNodeOpAmt + avaxNodeOpRewardAmt; Staking staking = Staking(getContractAddress("Staking")); staking.decreaseAVAXStake(owner, avaxNodeOpAmt); Vault vault = Vault(getContractAddress("Vault")); vault.withdrawAVAX(totalAvaxAmt); owner.safeTransferETH(totalAvaxAmt); }
Consider the following scenario User A first provides 1000 avax and calls createMinipool to create a minipool with a duration of 14 days After 14 days, user A is rewarded with 4 avax. User A considers triggering recreateMinipool and calls the withdrawMinipoolFunds function before recreateMinipool is called. At this point User A has withdrawn 1004 avax and avaxStaked is 0
staking.decreaseAVAXStake(owner, avaxNodeOpAmt); Vault vault = Vault(getContractAddress("Vault")); vault.withdrawAVAX(totalAvaxAmt); owner.safeTransferETH(totalAvaxAmt);
In the recreateMinipool function, user A's avaxStaked is increased to 4 and avaxAssigned is increased to 1004, avaxNodeOpAmt == avaxLiquidStakerAmt == 1004
uint256 compoundedAvaxNodeOpAmt = mp.avaxNodeOpAmt + mp.avaxNodeOpRewardAmt; setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpAmt")), compoundedAvaxNodeOpAmt); setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerAmt")), compoundedAvaxNodeOpAmt); Staking staking = Staking(getContractAddress("Staking")); // Only increase AVAX stake by rewards amount we are compounding // since AVAX stake is only decreased by withdrawMinipool() staking.increaseAVAXStake(mp.owner, mp.avaxNodeOpRewardAmt); staking.increaseAVAXAssigned(mp.owner, compoundedAvaxNodeOpAmt); staking.increaseMinipoolCount(mp.owner);
In claimAndInitiateStaking, the avaxStaked of user A is not checked and 1004 avax are taken directly from the Vault
function claimAndInitiateStaking(address nodeID) external { int256 minipoolIndex = onlyValidMultisig(nodeID); requireValidStateTransition(minipoolIndex, MinipoolStatus.Launched); uint256 avaxNodeOpAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpAmt"))); uint256 avaxLiquidStakerAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerAmt"))); // Transfer funds to this contract and then send to multisig ggAVAX.withdrawForStaking(avaxLiquidStakerAmt); addUint(keccak256("MinipoolManager.TotalAVAXLiquidStakerAmt"), avaxLiquidStakerAmt); setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Launched)); emit MinipoolStatusChanged(nodeID, MinipoolStatus.Launched); Vault vault = Vault(getContractAddress("Vault")); vault.withdrawAVAX(avaxNodeOpAmt); uint256 totalAvaxAmt = avaxNodeOpAmt + avaxLiquidStakerAmt; msg.sender.safeTransferETH(totalAvaxAmt); }
After 14 days, user A gets 4 avax rewards, in withdrawMinipoolFunds, at this time avaxNodeOpAmt == 1004, and avaxStaked == 4, in the call decreaseAVAXStake will fail due to overflow, but as long as the user creates another minipool with 1000 avax at this time, avaxStaked will increase to 1004, at this time call withdrawMinipoolFunds will successfully take out 1008 avax.
function withdrawMinipoolFunds(address nodeID) external nonReentrant { int256 minipoolIndex = requireValidMinipool(nodeID); address owner = onlyOwner(minipoolIndex); requireValidStateTransition(minipoolIndex, MinipoolStatus.Finished); setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Finished)); uint256 avaxNodeOpAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpAmt"))); uint256 avaxNodeOpRewardAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpRewardAmt"))); uint256 totalAvaxAmt = avaxNodeOpAmt + avaxNodeOpRewardAmt; Staking staking = Staking(getContractAddress("Staking")); staking.decreaseAVAXStake(owner, avaxNodeOpAmt); Vault vault = Vault(getContractAddress("Vault")); vault.withdrawAVAX(totalAvaxAmt); owner.safeTransferETH(totalAvaxAmt); }
In the second Staking, user A does not provide any avax and gets a double reward( 8 == 4*2)
https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/MinipoolManager.sol#L163-L167 https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/MinipoolManager.sol#L444-L478 https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/MinipoolManager.sol#L287-L303
None
Consider using a new state to ensure that recreateMinipool cannot be called after withdrawMinipoolFunds
#0 - c4-judge
2023-01-09T12:56:27Z
GalloDaSballo marked the issue as duplicate of #484
#1 - c4-judge
2023-02-03T12:41:06Z
GalloDaSballo marked the issue as duplicate of #569
#2 - c4-judge
2023-02-08T08:27:15Z
GalloDaSballo changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-02-08T20:15:55Z
GalloDaSballo marked the issue as satisfactory
#4 - c4-judge
2023-02-09T08:45:54Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#5 - Simon-Busch
2023-02-09T12:36:53Z
Changed the severity back to M as requested by @GalloDaSballo
118.8832 USDC - $118.88
When a NodeOP cannot guarantee 80% reliability, it will not be rewarded with avax, and in MinipoolManager.recordStakingEnd, the NodeOP will be slashed
if (avaxTotalRewardAmt == 0) { slash(minipoolIndex); }
In the slash function, the slash amount is 10% of avaxLiquidStakerAmt
setUint(keccak256("ProtocolDAO.ExpectedAVAXRewardsRate"), 0.1 ether); // Annual rate as pct of 1 avax ... function slash(int256 index) private { address nodeID = getAddress(keccak256(abi.encodePacked("minipool.item", index, ".nodeID"))); address owner = getAddress(keccak256(abi.encodePacked("minipool.item", index, ".owner"))); uint256 duration = getUint(keccak256(abi.encodePacked("minipool.item", index, ".duration"))); uint256 avaxLiquidStakerAmt = getUint(keccak256(abi.encodePacked("minipool.item", index, ".avaxLiquidStakerAmt"))); uint256 expectedAVAXRewardsAmt = getExpectedAVAXRewardsAmt(duration, avaxLiquidStakerAmt); uint256 slashGGPAmt = calculateGGPSlashAmt(expectedAVAXRewardsAmt); setUint(keccak256(abi.encodePacked("minipool.item", index, ".ggpSlashAmt")), slashGGPAmt); emit GGPSlashed(nodeID, slashGGPAmt); Staking staking = Staking(getContractAddress("Staking")); staking.slashGGP(owner, slashGGPAmt); }
But considering that the minimum required collateral rate for a NodeOP to create a minipool is 10% of avaxAssigned, and that avaxAssigned == avaxLiquidStakerAmt in general
uint256 ratio = staking.getCollateralizationRatio(msg.sender); if (ratio < dao.getMinCollateralizationRatio()) { // 10% revert InsufficientGGPCollateralization(); }
if the price of GGP drops during the time between the creation of the minipool and the slash, the amount of GGPs collateralized by the user will be less than the amount slashed, and the decreaseGGPStake function will fail due to overflow in the slashGGP function, thus causing the slash to fail
function slashGGP(address stakerAddr, uint256 ggpAmt) public onlySpecificRegisteredContract("MinipoolManager", msg.sender) { Vault vault = Vault(getContractAddress("Vault")); decreaseGGPStake(stakerAddr, ggpAmt); vault.transferToken("ProtocolDAO", ggp, ggpAmt); }
https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/MinipoolManager.sol#L670-L683 https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/Staking.sol#L379-L383
None
Consider the user's GGP balance in the slashGGP function
function slashGGP(address stakerAddr, uint256 ggpAmt) public onlySpecificRegisteredContract("MinipoolManager", msg.sender) { Vault vault = Vault(getContractAddress("Vault")); + ggpAmt = ggpAmt > getGGPStake(stakerAddr)? getGGPStake(stakerAddr): ggpAmt; decreaseGGPStake(stakerAddr, ggpAmt); vault.transferToken("ProtocolDAO", ggp, ggpAmt); }
#0 - c4-judge
2023-01-09T09:47:02Z
GalloDaSballo marked the issue as duplicate of #136
#1 - c4-judge
2023-02-03T19:37:03Z
GalloDaSballo marked the issue as not a duplicate
#2 - c4-judge
2023-02-03T19:40:43Z
GalloDaSballo changed the severity to 3 (High Risk)
#3 - c4-judge
2023-02-03T19:56:19Z
GalloDaSballo marked the issue as duplicate of #494
#4 - c4-judge
2023-02-03T19:58:50Z
GalloDaSballo changed the severity to 2 (Med Risk)
#5 - c4-judge
2023-02-08T20:16:53Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: 0xbepresent
Also found by: 0xbepresent, cccz, cozzetti, datapunk, hansfriese
492.1393 USDC - $492.14
The documentation says
NodeOps are eligible for a reward cycle if they have had at least 1 active minipool for a set amount of time, currently 14 days.
In the ClaimNodeOp.isEligible function, the RewardsStartTime is used to confirm whether NodeOps are eligible for GGP rewards
function isEligible(address stakerAddr) external view returns (bool) { Staking staking = Staking(getContractAddress("Staking")); uint256 rewardsStartTime = staking.getRewardsStartTime(stakerAddr); uint256 elapsedSecs = (block.timestamp - rewardsStartTime); ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO")); return (rewardsStartTime != 0 && elapsedSecs >= dao.getRewardsEligibilityMinSeconds()); // >= 14 days }
In the MinipoolManager contract, RewardsStartTime is set when the minipool is created
if (staking.getRewardsStartTime(msg.sender) == 0) { staking.setRewardsStartTime(msg.sender, block.timestamp); }
However, in the recordStakingError and _cancelMinipoolAndReturnFunds functions, when the minipool is cancelled, or when Staking error (the minipool is not closed due to the end of Staking, i.e. the minipool was not active), it does not reset the RewardsStartTime
function _cancelMinipoolAndReturnFunds(address nodeID, int256 index) private { requireValidStateTransition(index, MinipoolStatus.Canceled); setUint(keccak256(abi.encodePacked("minipool.item", index, ".status")), uint256(MinipoolStatus.Canceled)); address owner = getAddress(keccak256(abi.encodePacked("minipool.item", index, ".owner"))); uint256 avaxNodeOpAmt = getUint(keccak256(abi.encodePacked("minipool.item", index, ".avaxNodeOpAmt"))); uint256 avaxLiquidStakerAmt = getUint(keccak256(abi.encodePacked("minipool.item", index, ".avaxLiquidStakerAmt"))); Staking staking = Staking(getContractAddress("Staking")); staking.decreaseAVAXStake(owner, avaxNodeOpAmt); staking.decreaseAVAXAssigned(owner, avaxLiquidStakerAmt); staking.decreaseMinipoolCount(owner); emit MinipoolStatusChanged(nodeID, MinipoolStatus.Canceled); Vault vault = Vault(getContractAddress("Vault")); vault.withdrawAVAX(avaxNodeOpAmt); owner.safeTransferETH(avaxNodeOpAmt); } ... function recordStakingError(address nodeID, bytes32 errorCode) external payable { int256 minipoolIndex = onlyValidMultisig(nodeID); requireValidStateTransition(minipoolIndex, MinipoolStatus.Error); address owner = getAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".owner"))); uint256 avaxNodeOpAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpAmt"))); uint256 avaxLiquidStakerAmt = getUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerAmt"))); if (msg.value != (avaxNodeOpAmt + avaxLiquidStakerAmt)) { revert InvalidAmount(); } setBytes32(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".errorCode")), errorCode); setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".status")), uint256(MinipoolStatus.Error)); setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxTotalRewardAmt")), 0); setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxNodeOpRewardAmt")), 0); setUint(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".avaxLiquidStakerRewardAmt")), 0); // Send the nodeOps AVAX to vault so they can claim later Vault vault = Vault(getContractAddress("Vault")); vault.depositAVAX{value: avaxNodeOpAmt}(); // Return Liq stakers funds ggAVAX.depositFromStaking{value: avaxLiquidStakerAmt}(avaxLiquidStakerAmt, 0); Staking staking = Staking(getContractAddress("Staking")); staking.decreaseAVAXAssigned(owner, avaxLiquidStakerAmt); subUint(keccak256("MinipoolManager.TotalAVAXLiquidStakerAmt"), avaxLiquidStakerAmt); emit MinipoolStatusChanged(nodeID, MinipoolStatus.Error); }
In the calculateAndDistributeRewards function of the ClaimNodeOp contract, this means that the user will be rewarded with GGP even if there is no active minipool for that user.
// check if their rewards time should be reset uint256 minipoolCount = staking.getMinipoolCount(stakerAddr); if (minipoolCount == 0) { staking.setRewardsStartTime(stakerAddr, 0); }
https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/ClaimNodeOp.sol#L46-L52 https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/MinipoolManager.sol#L484-L515 https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/MinipoolManager.sol#L646-L665 https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/ClaimNodeOp.sol#L56-L85
None
In the recordStakingError and _cancelMinipoolAndReturnFunds functions, when minipool counts to 0, reset RewardsStartTime (need to decrease minipool count in recordStakingError, in a separate report)
#0 - c4-judge
2023-01-10T19:33:19Z
GalloDaSballo marked the issue as duplicate of #471
#1 - c4-judge
2023-02-08T20:11:00Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: HollaDieWaldfee
Also found by: Allarious, Aymen0909, Saintcode_, SmartSek, adriro, bin2chen, cccz, kaliberpoziomka8552, minhtrng, rvierdiiev, sces60107, sk8erboy, wagmi
68.0946 USDC - $68.09
In MinipoolManager, when a minipool is created, the minipool count is increased, and when Staking is ended or the minipool is canceled, the minipool count is decreased. However, in the recordStakingError function, the minipool count does not decrease if there is a Staking creation error.
Staking staking = Staking(getContractAddress("Staking")); staking.decreaseAVAXAssigned(owner, avaxLiquidStakerAmt); subUint(keccak256("MinipoolManager.TotalAVAXLiquidStakerAmt"), avaxLiquidStakerAmt);
This means that the minipool count of the user with Staking creation error will never be 0. In the calculateAndDistributeRewards function of the ClaimNodeOp contract, this means that the user will be rewarded with GGP even if there is no active minipool for that user.
// check if their rewards time should be reset uint256 minipoolCount = staking.getMinipoolCount(stakerAddr); if (minipoolCount == 0) { staking.setRewardsStartTime(stakerAddr, 0); }
https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/MinipoolManager.sol#L484-L515 https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/ClaimNodeOp.sol#L56-L85
None
Decrease the minipool count in MinipoolManager.recordStakingError
Staking staking = Staking(getContractAddress("Staking")); staking.decreaseAVAXAssigned(owner, avaxLiquidStakerAmt); + staking.decreaseMinipoolCount(owner); subUint(keccak256("MinipoolManager.TotalAVAXLiquidStakerAmt"), avaxLiquidStakerAmt);
#0 - 0xju1ie
2023-01-16T20:54:29Z
#1 - c4-judge
2023-01-31T15:14:42Z
GalloDaSballo marked the issue as duplicate of #235
#2 - c4-judge
2023-02-08T19:40:30Z
GalloDaSballo marked the issue as satisfactory