Platform: Code4rena
Start Date: 30/05/2023
Pot Size: $300,500 USDC
Total HM: 79
Participants: 101
Period: about 1 month
Judge: Trust
Total Solo HM: 36
Id: 242
League: ETH
Rank: 45/101
Findings: 1
Award: $592.67
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xTheC0der
Also found by: Verichains
592.6743 USDC - $592.67
https://github.com/code-423n4/2023-05-maia/blob/main/src/maia/vMaia.sol#L67-L88
The vMaia.sol
inherits as below:
vMaia <- ERC4626PartnerManager <- PartnerUtilityManager <- UtilityManager
In the vMaia contract, when users deposit Maia tokens, they receive vMaia tokens at a 1:1 ratio. By invoking specific claim...()
functions users are able to claim their Gauge Weight, Governance, and Partner Governance tokens.
Before executing a claim, the contract ensures that the token balance is greater than or equal to the total of the previously claimed tokens and the amount of tokens being claimed. However, it is important to note that the token balance should first be converted to bHermes tokens by multiplying it with the bHermesRate
before conducting the balance check.
// right logic amount = amount * bHermesRate; balanceOf[msg.sender] * bHermesRate < amount + userClaimedWeight[msg.sender];
Unfortunately, it seems that the vMaia contract overrides the checkWeight
and checkGovernance
, and checkPartnerGovernance
modifiers without converting the user's vMaia balance to bHermes.
// wrong override in vMaia contract balanceOf[msg.sender] < amount + userClaimedWeight[msg.sender];
When bHermesRate
greater than 1, call:
claimOutstanding()
: Revert due to balance always less than amount of tokens being claimed.claimMultiple()
, claimMultipleAmounts()
, claimWeight()
, claimGovernance()
, claimPartnerGovernance()
: Without multiplying the balance with the bHermesRate
, users receive less amount of Weight, Governance and Partner Governance tokens than they actually have to receive.Put the
PoC.sol
in ./test/maia folder
Run test:
forge test --match-path ./test/maia/PoC.sol --match-contract poc -vvv
PoC.sol
Deploy vMaia contract with
bHermesRate
is 2.
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {DSTestPlus} from "solmate/test/utils/DSTestPlus.sol"; import {MockERC20} from "solmate/test/utils/mocks/MockERC20.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {vMaia, PartnerManagerFactory, ERC20} from "@maia/vMaia.sol"; import {IBaseVault} from "@maia/interfaces/IBaseVault.sol"; import {MockVault} from "./mock/MockVault.t.sol"; import {bHermes} from "@hermes/bHermes.sol"; import {DateTimeLib} from "solady/utils/DateTimeLib.sol"; import {console2} from "forge-std/console2.sol"; contract poc is DSTestPlus { MockVault vault; MockERC20 public hermes; MockERC20 public maia; vMaia public vmaia; uint256 bHermesRate; bHermes public bhermes; function setUp() public { //1 jan 2023 hevm.warp(1672531200); hermes = new MockERC20("test hermes", "RTKN", 18); maia = new MockERC20("test maia", "tMAIA", 18); bhermes = new bHermes(hermes, address(this), 1 weeks, 1 days / 2); bHermesRate = 2; vmaia = new vMaia( PartnerManagerFactory(address(this)), bHermesRate, maia, "vote Maia", "vMAIA", address(bhermes), address(vault), address(0) ); // Transfer bHermes to vMaia contract hermes.mint(address(this), 1000 ether); hermes.approve(address(bhermes), 1000 ether); bhermes.deposit(1000 ether, address(this)); bhermes.transfer(address(vmaia), 1000 ether); } function testPoC() public { uint256 amount = 100 ether; uint256 expect = amount * bHermesRate; depositMaia(amount); // claim Outstanding hevm.expectRevert(abi.encodeWithSignature("InsufficientShares()")); vmaia.claimOutstanding(); console2.log("claimOutstanding(): reverted"); // claim Weight vmaia.claimWeight(amount); assertFalse( expect == ERC20(vmaia.gaugeWeight()).balanceOf(address(this)) ); console2.log( "claimWeight(): Expect %s, But receive %s", expect, ERC20(vmaia.gaugeWeight()).balanceOf(address(this)) ); // claim Governance vmaia.claimGovernance(amount); assertFalse( expect == ERC20(vmaia.governance()).balanceOf(address(this)) ); console2.log( "claimGovernance(): Expect %s, But receive %s", expect, ERC20(vmaia.governance()).balanceOf(address(this)) ); // claim PartnerGovernance vmaia.claimPartnerGovernance(amount); assertFalse( expect == ERC20(vmaia.partnerGovernance()).balanceOf(address(this)) ); console2.log( "claimPartnerGovernance(): Expect %s, But receive %s", expect, ERC20(vmaia.partnerGovernance()).balanceOf(address(this)) ); } function depositMaia(uint256 amount) internal { assertEq(vmaia.bHermesRate(), bHermesRate); maia.mint(address(this), amount); maia.approve(address(vmaia), amount); vmaia.deposit(amount, address(this)); assertEq(maia.balanceOf(address(vmaia)), amount); assertEq(vmaia.balanceOf(address(this)), amount); } }
Manual, Foundry
In Maia.sol#L67-88, the modifier can be fixed as below:
modifier checkWeight(uint256 amount) virtual override { - if (balanceOf[msg.sender] < amount + userClaimedWeight[msg.sender]) { + if (balanceOf[msg.sender] * bHermesRate < amount + userClaimedWeight[msg.sender]) { revert InsufficientShares(); } _; } /// @dev Checks available governance allows for the call. modifier checkGovernance(uint256 amount) virtual override { - if (balanceOf[msg.sender] < amount + userClaimedGovernance[msg.sender]) { + if (balanceOf[msg.sender] * bHermesRate < amount + userClaimedGovernance[msg.sender]) { revert InsufficientShares(); } _; } /// @dev Checks available partner governance allows for the call. modifier checkPartnerGovernance(uint256 amount) virtual override { - if (balanceOf[msg.sender] < amount + userClaimedPartnerGovernance[msg.sender]) { + if (balanceOf[msg.sender] * bHermesRate < amount + userClaimedPartnerGovernance[msg.sender]) { revert InsufficientShares(); } _; }
In ERC4626PartnerManager.sol
, claimOutstanding()
must be fixed and all of claim...
function must be overridden as below:
function claimOutstanding() public virtual { uint256 balance = balanceOf[msg.sender] * bHermesRate; /// @dev Never overflows since balandeOf >= userClaimed. - claimWeight(balance - userClaimedWeight[msg.sender]); + super.claimWeight(balance - userClaimedWeight[msg.sender]); claimBoost(balance - userClaimedBoost[msg.sender]); - claimGovernance(balance - userClaimedGovernance[msg.sender]); + super.claimGovernance(balance - userClaimedGovernance[msg.sender]); claimPartnerGovernance(balance - userClaimedPartnerGovernance[msg.sender]); } +function claimWeight(uint256 amount) public virtual override checkWeight(amount * bHermesRate) { + super.claimWeight(amount * bHermesRate); +} +function claimGovernance(uint256 amount) public virtual override checkGovernance(amount * bHermesRate) { + super.claimGovernance(amount * bHermesRate); +}
ERC4626
#0 - c4-judge
2023-07-09T11:30:43Z
trust1995 marked the issue as primary issue
#1 - c4-judge
2023-07-09T11:30:47Z
trust1995 marked the issue as satisfactory
#2 - c4-sponsor
2023-07-11T20:44:45Z
0xLightt marked the issue as sponsor confirmed
#3 - 0xLightt
2023-07-22T15:36:50Z
This is a duplicate of #470. The issue #470 has a more accurate solution
#4 - c4-judge
2023-07-25T07:54:28Z
trust1995 marked the issue as duplicate of #470
#5 - c4-judge
2023-07-25T14:07:20Z
trust1995 changed the severity to 2 (Med Risk)