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: 84/111
Findings: 2
Award: $36.62
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xdeadbeef0x
Also found by: 0xLad, 0xNazgul, 0xSmartContract, 0xbepresent, Arbor-Finance, Breeje, HE1M, IllIllI, Qeew, Rolezn, SEVEN, SamGMK, SmartSek, TomJ, WatchDogs, ak1, btk, ck, datapunk, dic0de, eierina, fs0c, hansfriese, koxuan, ladboy233, peanuts, rvierdiiev, sces60107, tonisives, unforgiven, yongskiws
14.9051 USDC - $14.91
If a malicious user is the first person who deposits AVAX in TokenggAVAX
, he can prevent other users from depositing by applying grieving attack.
ToeknggAVAX
.depositAVAX
.function depositAVAX() public payable returns (uint256 shares) { uint256 assets = msg.value; // Check for rounding error since we round down in previewDeposit. if ((shares = previewDeposit(assets)) == 0) { revert ZeroShares(); } emit Deposit(msg.sender, msg.sender, assets, shares); IWAVAX(address(asset)).deposit{value: assets}(); _mint(msg.sender, shares); afterDeposit(assets, shares); }
ggAVAX
will be minted to Bob. The state will be: totalSupply = 1
and totalAssets() = 1
. Please note that totalAssets
is increased by one because totalReleasedAssets
is increased in afterDeposit
.function previewDeposit(uint256 assets) public view virtual returns (uint256) { return convertToShares(assets); }
function convertToShares(uint256 assets) public view virtual returns (uint256) { uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. return supply == 0 ? assets : assets.mulDivDown(supply, totalAssets()); }
function afterDeposit( uint256 amount, uint256 /* shares */ ) internal override { totalReleasedAssets += amount; }
https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/tokens/TokenggAVAX.sol#L248 https://github.com/code-423n4/2022-12-gogopool/blob/aec9928d8bdce8a5a4efe45f54c39d4fc7313731/contracts/contract/tokens/TokenggAVAX.sol#L129
depositAVAX
) transfers 100 AVAX to the contract TokenggAVAX
. So the state will be: totalSupply = 1
and totalAssets() = 101
100 * 1 /(101) = 0
. So Alice's transaction will be failed due to ZeroShares
error.if ((shares = previewDeposit(assets)) == 0) { revert ZeroShares(); }
withdrawAVAX(101)
. By doing so, the state will be: totalSupply = 0
and totalAssets() = 0
.function withdrawAVAX(uint256 assets) public returns (uint256 shares) { shares = previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up. beforeWithdraw(assets, shares); _burn(msg.sender, shares); emit Withdraw(msg.sender, msg.sender, msg.sender, assets, shares); IWAVAX(address(asset)).withdraw(assets); msg.sender.safeTransferETH(assets); }
In summary, Bob can apply this grieving attack to all the users who are going to deposit, and preventing them from depositing in the protocol.
First Solution:
Alice's deposit * Bob's first deposit / (Bob's first deposit + Bob's direct transfer) < 1 ==> Bob's direct transfer = Bob's first deposit * (Alice's deposit - 1) + 1
10 * (100 - 1) + 1 = 991
, this makes the attack for Bob much more difficult.Second Solution:
To use MINIMUM_LIQUIDITY like unsiwap V2:
if (_totalSupply == 0) { liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens } else { liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); }
#0 - c4-judge
2023-01-08T13:12:08Z
GalloDaSballo marked the issue as duplicate of #209
#1 - c4-judge
2023-01-29T18:38:51Z
GalloDaSballo changed the severity to 3 (High Risk)
#2 - c4-judge
2023-02-08T09:44:57Z
GalloDaSballo marked the issue as satisfactory
🌟 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
During upgrading the existing contract, first the new address is registered, then the old address is unregistered. If the name of the contract is going to be the same (it should be the same as it is hardcoded in the contract Ocyticus.sol
), during unregistering the address of the contract will be set to address(0).
To restrict actions in important contracts, there is the function:
function pauseEverything() external onlyDefender { ProtocolDAO dao = ProtocolDAO(getContractAddress("ProtocolDAO")); dao.pauseContract("TokenggAVAX"); dao.pauseContract("MinipoolManager"); dao.pauseContract("Staking"); disableAllMultisigs(); }
It means that the important contracts will have the name TokenggAVAX
, MinipoolManager
, and Staking
. In other words, if they are going to be deployed on different address, they should have still the same name, otherwise the contract Ocyticus.sol
should be redeployed with the new names of these contracts, because the name of these contracts are hardcoded.
There is another function to upgrade the name and address of the contracts in the storage.
function upgradeExistingContract( address newAddr, string memory newName, address existingAddr ) external onlyGuardian { registerContract(newAddr, newName); unregisterContract(existingAddr); }
The problem is that if for example the contract TokenggAVAX.sol
is going to be upgraded, the function above will be called with the following parameters:
newAddr
= new address of the contracts TokenggAVAX.sol
newName
= TokenggAVAX (this should not be changed, otherwise the contract Ocyticus.sol
should be redeployed as the name TokenggAVAX is hardcoded)existingAddr
= old address of the contract TokenggAVAX.sol
In this function, first the new address will be registered:
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); }
So, we will have:
booleanStorage[H("contract.exists", new address)]
= trueaddressStorage[H("contract.address", "TokenggAVAX")]
= new addressstringStorage[H("contract.name", new address)]
= "TokenggAVAX"Then, the old address is unregistered.
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))); }
So, we will have:
booleanStorage[H("contract.exists", old address)]
= falseaddressStorage[H("contract.address", "TokenggAVAX")]
= address(0)stringStorage[H("contract.name", old address)]
= ""As you see, the problem is that the address of the contract TokenggAVAX.sol
is set to address(0), while it was supposed to be equal to the new address.
The following modifications should be applied (first should be unregistered, and then should be registered):
//before function upgradeExistingContract( address newAddr, string memory newName, address existingAddr ) external onlyGuardian { registerContract(newAddr, newName); unregisterContract(existingAddr); } //after function upgradeExistingContract( address newAddr, string memory newName, address existingAddr ) external onlyGuardian { unregisterContract(existingAddr); registerContract(newAddr, newName); }
#0 - c4-judge
2023-01-09T10:05:14Z
GalloDaSballo marked the issue as duplicate of #742
#1 - c4-judge
2023-02-08T20:09:33Z
GalloDaSballo marked the issue as satisfactory