diff --git a/.vscode/settings.json b/.vscode/settings.json index 003dda3..48f71ed 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,7 @@ "solidity.remappings": [ "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/", "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", - "@symbiotic/=lib/core/src/" + "@symbiotic/=lib/core/src/", + "@symbiotic-burners/=lib/burners/src/" ] } diff --git a/remappings.txt b/remappings.txt index 202f801..09404fd 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,4 @@ @openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/ @openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ @symbiotic/=lib/core/src/ +@symbiotic-burners/=lib/burners/src/ diff --git a/script/Delegator&Slasher&VaultFactory.s.sol b/script/Delegator&Slasher&VaultFactory.s.sol new file mode 100644 index 0000000..2b82a8e --- /dev/null +++ b/script/Delegator&Slasher&VaultFactory.s.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.25; + +import {Script, console2} from "forge-std/Script.sol"; +import {DelegatorFactory} from "@symbiotic/contracts/DelegatorFactory.sol"; +import {SlasherFactory} from "@symbiotic/contracts/SlasherFactory.sol"; +import {VaultFactory} from "@symbiotic/contracts/VaultFactory.sol"; + +contract DeployFactories is Script { + address constant OWNER = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + + function run() external { + uint256 deployerPrivateKey = uint256(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80); + vm.startBroadcast(deployerPrivateKey); + + DelegatorFactory delegatorFactory = new DelegatorFactory(OWNER); + console2.log("DelegatorFactory deployed at:", address(delegatorFactory)); + + SlasherFactory slasherFactory = new SlasherFactory(OWNER); + console2.log("SlasherFactory deployed at:", address(slasherFactory)); + + VaultFactory vaultFactory = new VaultFactory(OWNER); + console2.log("VaultFactory deployed at:", address(vaultFactory)); + + vm.stopBroadcast(); + } +} diff --git a/script/Deploy_All.s.sol b/script/Deploy_All.s.sol new file mode 100644 index 0000000..24a9737 --- /dev/null +++ b/script/Deploy_All.s.sol @@ -0,0 +1,180 @@ +pragma solidity ^0.8.25; + +import {Script, console2} from "forge-std/Script.sol"; +import {VaultFactory} from "@symbiotic/contracts/VaultFactory.sol"; +import {DelegatorFactory} from "@symbiotic/contracts/DelegatorFactory.sol"; +import {SlasherFactory} from "@symbiotic/contracts/SlasherFactory.sol"; +import {IMigratablesFactory} from "@symbiotic/interfaces/common/IMigratablesFactory.sol"; +import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; +import {IVaultConfigurator} from "@symbiotic/interfaces/IVaultConfigurator.sol"; +import {IBaseDelegator} from "@symbiotic/interfaces/delegator/IBaseDelegator.sol"; +import {INetworkRestakeDelegator} from "@symbiotic/interfaces/delegator/INetworkRestakeDelegator.sol"; +import {IFullRestakeDelegator} from "@symbiotic/interfaces/delegator/IFullRestakeDelegator.sol"; +import {IOperatorSpecificDelegator} from "@symbiotic/interfaces/delegator/IOperatorSpecificDelegator.sol"; +import {IBaseSlasher} from "@symbiotic/interfaces/slasher/IBaseSlasher.sol"; +import {ISlasher} from "@symbiotic/interfaces/slasher/ISlasher.sol"; +import {IVetoSlasher} from "@symbiotic/interfaces/slasher/IVetoSlasher.sol"; + +import {iBTC_Treasury} from "../src/iBTC_Treasury.sol"; +import {iBTC_Burner} from "../src/iBTC_Burner.sol"; +import {VaultConfigurator} from "../src/iBTC_VaultConfigurator.sol"; +import {iBTC_Vault} from "../src/iBTC_Vault.sol"; + +contract DeployAll is Script { + // Define deployment parameters + address constant COLLATERAL_ADDRESS = 0xeb762Ed11a09E4A394C9c8101f8aeeaf5382ED74; // eth sepolia + uint256 constant MAX_WITHDRAW_AMOUNT = 1e9; // 10 iBTC + uint256 constant MIN_WITHDRAW_AMOUNT = 1e4; + uint256 deployerPrivateKey = uint256(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80); //NOTE + + // Replace with the correct checksummed addresses + address constant VAULT_FACTORY = 0x407A039D94948484D356eFB765b3c74382A050B4; // Replace with deployed VaultFactory address + address constant DELEGATOR_FACTORY = 0x890CA3f95E0f40a79885B7400926544B2214B03f; // Replace with deployed DelegatorFactory address + address constant SLASHER_FACTORY = 0xbf34bf75bb779c383267736c53a4ae86ac7bB299; // Replace with deployed SlasherFactory address + + function run() external { + address[] memory whitelistedDepositors; + address owner = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; //NOTE + address collateral = 0xeb762Ed11a09E4A394C9c8101f8aeeaf5382ED74; + uint48 epochDuration = 604_800; // 7days + uint256 depositLimit = 1e9; // 10iBTC + uint64 delegatorIndex = 0; // NetworkRestakeDelegator + address hook = 0x0000000000000000000000000000000000000000; + bool withSlasher = true; + uint64 slasherIndex = 1; // vetoSlasher + uint48 vetoDuration = 86_400; // 1 day + vm.startBroadcast(deployerPrivateKey); + + // Deploy the iBTC_Treasury contract + iBTC_Treasury treasury = new iBTC_Treasury(COLLATERAL_ADDRESS, MAX_WITHDRAW_AMOUNT, MIN_WITHDRAW_AMOUNT); + + // Deploy the iBTC_Burner contract + iBTC_Burner burner = new iBTC_Burner(COLLATERAL_ADDRESS, address(treasury)); + + VaultConfigurator vaultConfigurator = new VaultConfigurator(VAULT_FACTORY, DELEGATOR_FACTORY, SLASHER_FACTORY); + + // Log the deployed address + console2.log("VaultConfigurator deployed at:", address(vaultConfigurator)); + (,, address deployer) = vm.readCallers(); + + bool depositWhitelist = whitelistedDepositors.length != 0; + + bytes memory vaultParams = abi.encode( + IVault.InitParams({ + collateral: collateral, + burner: address(burner), + epochDuration: epochDuration, + depositWhitelist: depositWhitelist, + isDepositLimit: depositLimit != 0, + depositLimit: depositLimit, + defaultAdminRoleHolder: depositWhitelist ? deployer : owner, + depositWhitelistSetRoleHolder: owner, + depositorWhitelistRoleHolder: owner, + isDepositLimitSetRoleHolder: owner, + depositLimitSetRoleHolder: owner + }) + ); + + uint256 roleHolders = 1; + if (hook != address(0) && hook != owner) { + roleHolders = 2; + } + address[] memory networkLimitSetRoleHolders = new address[](roleHolders); + address[] memory operatorNetworkLimitSetRoleHolders = new address[](roleHolders); + address[] memory operatorNetworkSharesSetRoleHolders = new address[](roleHolders); + networkLimitSetRoleHolders[0] = owner; + operatorNetworkLimitSetRoleHolders[0] = owner; + operatorNetworkSharesSetRoleHolders[0] = owner; + if (roleHolders > 1) { + networkLimitSetRoleHolders[1] = hook; + operatorNetworkLimitSetRoleHolders[1] = hook; + operatorNetworkSharesSetRoleHolders[1] = hook; + } + + bytes memory delegatorParams; + if (delegatorIndex == 0) { + delegatorParams = abi.encode( + INetworkRestakeDelegator.InitParams({ + baseParams: IBaseDelegator.BaseParams({ + defaultAdminRoleHolder: owner, + hook: hook, + hookSetRoleHolder: owner + }), + networkLimitSetRoleHolders: networkLimitSetRoleHolders, + operatorNetworkSharesSetRoleHolders: operatorNetworkSharesSetRoleHolders + }) + ); + } else if (delegatorIndex == 1) { + delegatorParams = abi.encode( + IFullRestakeDelegator.InitParams({ + baseParams: IBaseDelegator.BaseParams({ + defaultAdminRoleHolder: owner, + hook: hook, + hookSetRoleHolder: owner + }), + networkLimitSetRoleHolders: networkLimitSetRoleHolders, + operatorNetworkLimitSetRoleHolders: operatorNetworkLimitSetRoleHolders + }) + ); + } else if (delegatorIndex == 2) { + delegatorParams = abi.encode( + IOperatorSpecificDelegator.InitParams({ + baseParams: IBaseDelegator.BaseParams({ + defaultAdminRoleHolder: owner, + hook: hook, + hookSetRoleHolder: owner + }), + networkLimitSetRoleHolders: networkLimitSetRoleHolders, + operator: owner + }) + ); + } + + bytes memory slasherParams; + if (slasherIndex == 0) { + slasherParams = abi.encode( + ISlasher.InitParams({baseParams: IBaseSlasher.BaseParams({isBurnerHook: address(burner) != address(0)})}) + ); + } else if (slasherIndex == 1) { + slasherParams = abi.encode( + IVetoSlasher.InitParams({ + baseParams: IBaseSlasher.BaseParams({isBurnerHook: address(burner) != address(0)}), + vetoDuration: vetoDuration, + resolverSetEpochsDelay: 3 + }) + ); + } + + (address vault_, address delegator_, address slasher_) = IVaultConfigurator(vaultConfigurator).create( + IVaultConfigurator.InitParams({ + version: 1, + owner: owner, + vaultParams: vaultParams, + delegatorIndex: delegatorIndex, + delegatorParams: delegatorParams, + withSlasher: withSlasher, + slasherIndex: slasherIndex, + slasherParams: slasherParams + }) + ); + + if (depositWhitelist) { + iBTC_Vault(vault_).grantRole(iBTC_Vault(vault_).DEFAULT_ADMIN_ROLE(), owner); + iBTC_Vault(vault_).grantRole(iBTC_Vault(vault_).DEPOSITOR_WHITELIST_ROLE(), deployer); + + for (uint256 i; i < whitelistedDepositors.length; ++i) { + iBTC_Vault(vault_).setDepositorWhitelistStatus(whitelistedDepositors[i], true); + } + + iBTC_Vault(vault_).renounceRole(iBTC_Vault(vault_).DEPOSITOR_WHITELIST_ROLE(), deployer); + iBTC_Vault(vault_).renounceRole(iBTC_Vault(vault_).DEFAULT_ADMIN_ROLE(), deployer); + } + + console2.log("Vault: ", vault_); + console2.log("Delegator: ", delegator_); + console2.log("Slasher: ", slasher_); + + // Stop broadcasting transactions + vm.stopBroadcast(); + } +} diff --git a/script/iBTC_Burner.s.sol b/script/iBTC_Burner.s.sol new file mode 100644 index 0000000..ce2c190 --- /dev/null +++ b/script/iBTC_Burner.s.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; +import {iBTC_Treasury} from "../src/iBTC_Treasury.sol"; +import {iBTC_Burner} from "../src/iBTC_Burner.sol"; + +contract DeployiBTC_Burner is Script { + // Define deployment parameters + address constant COLLATERAL_ADDRESS = 0xeb762Ed11a09E4A394C9c8101f8aeeaf5382ED74; // eth sepolia + uint256 constant MAX_WITHDRAW_AMOUNT = 1e9; // 10 iBTC + uint256 constant MIN_WITHDRAW_AMOUNT = 1e4; + + function run() external { + // Fetch the private key to deploy contracts + uint256 deployerPrivateKey = uint256(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80); + + // Start broadcasting transactions from the deployer account + vm.startBroadcast(deployerPrivateKey); + + // Deploy the iBTC_Treasury contract + iBTC_Treasury treasury = new iBTC_Treasury(COLLATERAL_ADDRESS, MAX_WITHDRAW_AMOUNT, MIN_WITHDRAW_AMOUNT); + + // Deploy the iBTC_Burner contract + iBTC_Burner burner = new iBTC_Burner(COLLATERAL_ADDRESS, address(treasury)); + + // Stop broadcasting transactions + vm.stopBroadcast(); + } +} diff --git a/script/iBTC_Configurator.s.sol b/script/iBTC_Configurator.s.sol new file mode 100644 index 0000000..652daa6 --- /dev/null +++ b/script/iBTC_Configurator.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.25; + +import {Script, console2} from "forge-std/Script.sol"; +import {VaultConfigurator} from "../src/iBTC_VaultConfigurator.sol"; +import {VaultFactory} from "@symbiotic/contracts/VaultFactory.sol"; +import {DelegatorFactory} from "@symbiotic/contracts/DelegatorFactory.sol"; +import {SlasherFactory} from "@symbiotic/contracts/SlasherFactory.sol"; + +contract DeployVaultConfigurator is Script { + // Replace with the correct checksummed addresses + address constant VAULT_FACTORY = 0x407A039D94948484D356eFB765b3c74382A050B4; // Replace with deployed VaultFactory address + address constant DELEGATOR_FACTORY = 0x890CA3f95E0f40a79885B7400926544B2214B03f; // Replace with deployed DelegatorFactory address + address constant SLASHER_FACTORY = 0xbf34bf75bb779c383267736c53a4ae86ac7bB299; // Replace with deployed SlasherFactory address + + function run() external { + uint256 deployerPrivateKey = uint256(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80); // Load the private key from the environment + vm.startBroadcast(deployerPrivateKey); + + // Deploy the VaultConfigurator contract + VaultConfigurator vaultConfigurator = new VaultConfigurator(VAULT_FACTORY, DELEGATOR_FACTORY, SLASHER_FACTORY); + + // Log the deployed address + console2.log("VaultConfigurator deployed at:", address(vaultConfigurator)); + + vm.stopBroadcast(); + } +} diff --git a/script/iBTC_Vault.s.sol b/script/iBTC_Vault.s.sol new file mode 100644 index 0000000..cd6be3b --- /dev/null +++ b/script/iBTC_Vault.s.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.25; + +import {Script, console2} from "forge-std/Script.sol"; + +import {IMigratablesFactory} from "@symbiotic/interfaces/common/IMigratablesFactory.sol"; +import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; +import {IVaultConfigurator} from "@symbiotic/interfaces/IVaultConfigurator.sol"; +import {IBaseDelegator} from "@symbiotic/interfaces/delegator/IBaseDelegator.sol"; +import {INetworkRestakeDelegator} from "@symbiotic/interfaces/delegator/INetworkRestakeDelegator.sol"; +import {IFullRestakeDelegator} from "@symbiotic/interfaces/delegator/IFullRestakeDelegator.sol"; +import {IOperatorSpecificDelegator} from "@symbiotic/interfaces/delegator/IOperatorSpecificDelegator.sol"; +import {IBaseSlasher} from "@symbiotic/interfaces/slasher/IBaseSlasher.sol"; +import {ISlasher} from "@symbiotic/interfaces/slasher/ISlasher.sol"; +import {IVetoSlasher} from "@symbiotic/interfaces/slasher/IVetoSlasher.sol"; + +import {iBTC_Vault} from "../src/iBTC_Vault.sol"; + +contract VaultScript is Script { + function run() public { + address vaultConfigurator = 0xe7Fc028421E9efE4b9713a577B081a0E0D6a1C82; + address owner = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; + address collateral = 0xeb762Ed11a09E4A394C9c8101f8aeeaf5382ED74; + address burner = 0x81295Ffa36826dc643CF8CFcBF22e08227b91851; + uint48 epochDuration = 604_800; // 7days + address[] memory whitelistedDepositors; + uint256 depositLimit = 1e9; // 10iBTC + uint64 delegatorIndex = 0; // NetworkRestakeDelegator + address hook = 0x0000000000000000000000000000000000000000; + bool withSlasher = true; + uint64 slasherIndex = 1; // vetoSlasher + uint48 vetoDuration = 86_400; // 1 day + vm.startBroadcast(); + (,, address deployer) = vm.readCallers(); + + bool depositWhitelist = whitelistedDepositors.length != 0; + + bytes memory vaultParams = abi.encode( + IVault.InitParams({ + collateral: collateral, + burner: burner, + epochDuration: epochDuration, + depositWhitelist: depositWhitelist, + isDepositLimit: depositLimit != 0, + depositLimit: depositLimit, + defaultAdminRoleHolder: depositWhitelist ? deployer : owner, + depositWhitelistSetRoleHolder: owner, + depositorWhitelistRoleHolder: owner, + isDepositLimitSetRoleHolder: owner, + depositLimitSetRoleHolder: owner + }) + ); + + uint256 roleHolders = 1; + if (hook != address(0) && hook != owner) { + roleHolders = 2; + } + address[] memory networkLimitSetRoleHolders = new address[](roleHolders); + address[] memory operatorNetworkLimitSetRoleHolders = new address[](roleHolders); + address[] memory operatorNetworkSharesSetRoleHolders = new address[](roleHolders); + networkLimitSetRoleHolders[0] = owner; + operatorNetworkLimitSetRoleHolders[0] = owner; + operatorNetworkSharesSetRoleHolders[0] = owner; + if (roleHolders > 1) { + networkLimitSetRoleHolders[1] = hook; + operatorNetworkLimitSetRoleHolders[1] = hook; + operatorNetworkSharesSetRoleHolders[1] = hook; + } + + bytes memory delegatorParams; + if (delegatorIndex == 0) { + delegatorParams = abi.encode( + INetworkRestakeDelegator.InitParams({ + baseParams: IBaseDelegator.BaseParams({ + defaultAdminRoleHolder: owner, + hook: hook, + hookSetRoleHolder: owner + }), + networkLimitSetRoleHolders: networkLimitSetRoleHolders, + operatorNetworkSharesSetRoleHolders: operatorNetworkSharesSetRoleHolders + }) + ); + } else if (delegatorIndex == 1) { + delegatorParams = abi.encode( + IFullRestakeDelegator.InitParams({ + baseParams: IBaseDelegator.BaseParams({ + defaultAdminRoleHolder: owner, + hook: hook, + hookSetRoleHolder: owner + }), + networkLimitSetRoleHolders: networkLimitSetRoleHolders, + operatorNetworkLimitSetRoleHolders: operatorNetworkLimitSetRoleHolders + }) + ); + } else if (delegatorIndex == 2) { + delegatorParams = abi.encode( + IOperatorSpecificDelegator.InitParams({ + baseParams: IBaseDelegator.BaseParams({ + defaultAdminRoleHolder: owner, + hook: hook, + hookSetRoleHolder: owner + }), + networkLimitSetRoleHolders: networkLimitSetRoleHolders, + operator: owner + }) + ); + } + + bytes memory slasherParams; + if (slasherIndex == 0) { + slasherParams = abi.encode( + ISlasher.InitParams({baseParams: IBaseSlasher.BaseParams({isBurnerHook: burner != address(0)})}) + ); + } else if (slasherIndex == 1) { + slasherParams = abi.encode( + IVetoSlasher.InitParams({ + baseParams: IBaseSlasher.BaseParams({isBurnerHook: burner != address(0)}), + vetoDuration: vetoDuration, + resolverSetEpochsDelay: 3 + }) + ); + } + + (address vault_, address delegator_, address slasher_) = IVaultConfigurator(vaultConfigurator).create( + IVaultConfigurator.InitParams({ + version: 1, + owner: owner, + vaultParams: vaultParams, + delegatorIndex: delegatorIndex, + delegatorParams: delegatorParams, + withSlasher: withSlasher, + slasherIndex: slasherIndex, + slasherParams: slasherParams + }) + ); + + if (depositWhitelist) { + iBTC_Vault(vault_).grantRole(iBTC_Vault(vault_).DEFAULT_ADMIN_ROLE(), owner); + iBTC_Vault(vault_).grantRole(iBTC_Vault(vault_).DEPOSITOR_WHITELIST_ROLE(), deployer); + + for (uint256 i; i < whitelistedDepositors.length; ++i) { + iBTC_Vault(vault_).setDepositorWhitelistStatus(whitelistedDepositors[i], true); + } + + iBTC_Vault(vault_).renounceRole(iBTC_Vault(vault_).DEPOSITOR_WHITELIST_ROLE(), deployer); + iBTC_Vault(vault_).renounceRole(iBTC_Vault(vault_).DEFAULT_ADMIN_ROLE(), deployer); + } + + console2.log("Vault: ", vault_); + console2.log("Delegator: ", delegator_); + console2.log("Slasher: ", slasher_); + + vm.stopBroadcast(); + } +} diff --git a/src/iBTC_Burner.sol b/src/iBTC_Burner.sol new file mode 100644 index 0000000..9107675 --- /dev/null +++ b/src/iBTC_Burner.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +// import {SelfDestruct} from "src/common/SelfDestruct.sol"; we don't need selfDestruct +import {UintRequests} from "@symbiotic-burners/contracts/common/UintRequests.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {IiBTC_Burner} from "./interfaces/IiBTC_Burner.sol"; +import {IiBTC_Treasury} from "./interfaces/IiBTC_Treasury.sol"; + +contract iBTC_Burner is UintRequests, IiBTC_Burner, IERC721Receiver { + using Math for uint256; + + address public immutable Collateral; + + address public immutable IBTCTreasury; + + constructor(address collateral, address iBTCtreasury) { + Collateral = collateral; + + IBTCTreasury = iBTCtreasury; + + IERC20(Collateral).approve(IBTCTreasury, type(uint256).max); + } + + /** + * IiBTC_Burner + * This function triggers a withdrawal by creating one or more withdrawal requests. + * It splits the total collateral balance into multiple requests based on the maximum withdrawal limit. + */ + function triggerWithdrawal( + uint256 maxRequests + ) external returns (uint256 firstRequestId, uint256 lastRequestId) { + // Get the current balance of the COLLATERAL token held by this contract + uint256 amount = IERC20(Collateral).balanceOf(address(this)); + + // Fetch the maximum and minimum withdrawal amounts from the treasury + uint256 maxWithdrawalAmount = IiBTC_Treasury(IBTCTreasury).withdrawRequestMaximum(); + uint256 minWithdrawalAmount = IiBTC_Treasury(IBTCTreasury).withdrawRequestMinimum(); + + // Calculate the number of full requests that can be made using the maximum withdrawal amount + uint256 requests = amount / maxWithdrawalAmount; + + // If there's a remaining amount greater than the minimum withdrawal amount, add an additional request + if (amount % maxWithdrawalAmount >= minWithdrawalAmount) { + requests += 1; + } + + // Ensure the number of requests does not exceed the user-specified maximum (`maxRequests`) + requests = Math.min(requests, maxRequests); + + // If no requests can be made (e.g., insufficient collateral), revert with an error + if (requests == 0) { + revert InsufficientWithdrawal(); + } + + // Calculate the range of request IDs that will be created + uint256 requestsMinusOne = requests - 1; // Total requests minus one + firstRequestId = IiBTC_Treasury(IBTCTreasury).getLastrequestIdCreated() + 1; // First request ID + lastRequestId = firstRequestId + requestsMinusOne; // Last request ID + + // Initialize `requestId` with the first request ID + uint256 requestId = firstRequestId; + + // Loop through all but the last request and create withdrawal requests with the maximum withdrawal amount + for (; requestId < lastRequestId; ++requestId) { + // Add the request ID to the tracking list + _addRequestId(requestId); + + // Create a withdrawal request for the maximum withdrawal amount + IiBTC_Treasury(IBTCTreasury).createWithdrawRequest(maxWithdrawalAmount); + } + + // Add the final request ID to the tracking list + _addRequestId(requestId); + + // For the last request, calculate the remaining amount and ensure it doesn't exceed the maximum limit + IiBTC_Treasury(IBTCTreasury).createWithdrawRequest( + Math.min(amount - requestsMinusOne * maxWithdrawalAmount, maxWithdrawalAmount) + ); + + // Emit an event to record the range of request IDs that were created + emit TriggerWithdrawal(msg.sender, firstRequestId, lastRequestId); + + // Return the first and last request IDs to the caller + return (firstRequestId, lastRequestId); + } + + // function onSlash(bytes32 subnetwork, address operator, uint256 amount, uint48 captureTimestamp) external; + + /** + * @notice Get an address of the collateral. + */ + function COLLATERAL() external view returns (address) { + return Collateral; + } + + /** + * @notice Get an address of the dlcBTC Exit contract. + */ + function iBTCTreasury() external view returns (address) { + return IBTCTreasury; + } + + /** + * @inheritdoc IERC721Receiver + */ + function onERC721Received(address, address, uint256, bytes calldata) external view returns (bytes4) { + return IERC721Receiver.onERC721Received.selector; + } + + receive() external payable {} +} diff --git a/src/iBTC_Treasury.sol b/src/iBTC_Treasury.sol new file mode 100644 index 0000000..9b782c2 --- /dev/null +++ b/src/iBTC_Treasury.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./libraries/Counter.sol"; + +contract iBTC_Treasury is ERC721, Ownable { + using Counters for Counters.Counter; + + // Counter for the token IDs of withdrawal requests + Counters.Counter private _requestIds; + + // ERC20 token used as collateral + IERC20 public immutable collateral; + + // Maximum and minimum amounts for withdrawal requests + uint256 public maxWithdrawAmount; + uint256 public minWithdrawAmount; + mapping(uint256 requestId => uint256 withdrawalAmounts) public withdrawalRequests; + + mapping(uint256 requestId => bool hasFinalized) public finalizedWithdrawals; + + // Event emitted when a withdrawal request is created + event WithdrawalRequestCreated(address indexed requester, uint256 requestId, uint256 amount); + + // Event emitted when a withdrawal is finalized + event WithdrawalFinalized(address indexed requester, uint256 requestId, uint256 amount); + + constructor( + address _collateral, + uint256 _maxWithdrawAmount, + uint256 _minWithdrawAmount + ) ERC721("iBTC Withdrawal Request", "iBTC-WR") Ownable(msg.sender) { + // Ensure that the maximum withdrawal amount is greater than the minimum + require(_maxWithdrawAmount > _minWithdrawAmount, "Invalid withdrawal limits"); + + collateral = IERC20(_collateral); + maxWithdrawAmount = _maxWithdrawAmount; + minWithdrawAmount = _minWithdrawAmount; + } + + /** + * @dev Returns the last request ID created for a withdrawal request. + */ + function getLastrequestIdCreated() external view returns (uint256) { + return _requestIds.current(); + } + + /** + * @dev Updates the maximum and minimum withdrawal limits. + * Only the contract owner can call this function. + */ + function setWithdrawalLimits(uint256 _maxWithdrawAmount, uint256 _minWithdrawAmount) external onlyOwner { + require(_maxWithdrawAmount > _minWithdrawAmount, "Invalid withdrawal limits"); + maxWithdrawAmount = _maxWithdrawAmount; + minWithdrawAmount = _minWithdrawAmount; + } + + /** + * @dev Allows users to create a withdrawal request. + * Transfers collateral from the caller to the treasury and mints an ERC721 token. + */ + function createWithdrawRequest( + uint256 amount + ) external { + require(amount >= minWithdrawAmount, "Amount below minimum limit"); + require(amount <= maxWithdrawAmount, "Amount exceeds maximum limit"); + + // Transfer the collateral from the user to the treasury + require(collateral.transferFrom(msg.sender, address(this), amount), "Transfer failed"); + + // Increment the token ID counter + _requestIds.increment(); + uint256 requestId = _requestIds.current(); + + // Store the withdrawal amount associated with the token ID + withdrawalRequests[requestId] = amount; + + // Mint an ERC721 token representing the withdrawal request + _mint(msg.sender, requestId); + + emit WithdrawalRequestCreated(msg.sender, requestId, amount); + } + + /** + * @dev Finalizes a withdrawal request and burns the corresponding token. + * Transfers the collateral back to the token owner. + */ + function finalizeWithdrawal( + uint256 requestId + ) external onlyOwner { + require(!finalizedWithdrawals[requestId], "Already finalized"); + + address requester = ownerOf(requestId); + uint256 amount = withdrawalRequests[requestId]; + require(amount > 0, "Invalid withdrawal request"); + + // Mark the withdrawal as finalized + finalizedWithdrawals[requestId] = true; + + // Burn the ERC721 token representing the request + _burn(requestId); + + // Transfer the collateral back to the requester + require(collateral.transfer(requester, amount), "Transfer failed"); + + emit WithdrawalFinalized(requester, requestId, amount); + } + + /** + * @dev Batch process withdrawals up to a specified token ID. + * Only callable by the contract owner. + */ + function processWithdrawals( + uint256 _lastrequestIdToProcess + ) external onlyOwner { + for (uint256 requestId = 1; requestId <= _lastrequestIdToProcess; requestId++) { + if (_exits(requestId) && !finalizedWithdrawals[requestId]) { + address requester = ownerOf(requestId); + uint256 amount = withdrawalRequests[requestId]; + + // Finalize the withdrawal + finalizedWithdrawals[requestId] = true; + _burn(requestId); + require(collateral.transfer(requester, amount), "Transfer failed"); + + emit WithdrawalFinalized(requester, requestId, amount); + } + } + } + + function withdrawRequestMaximum() external view returns (uint256) { + return maxWithdrawAmount; + } + + function withdrawRequestMinimum() external view returns (uint256) { + return minWithdrawAmount; + } + + function _exits( + uint256 requestId + ) internal view returns (bool) { + return ownerOf(requestId) != address(0); + } +} diff --git a/src/iBTC_Vault.sol b/src/iBTC_Vault.sol new file mode 100644 index 0000000..5d58e47 --- /dev/null +++ b/src/iBTC_Vault.sol @@ -0,0 +1,484 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.25; + +import {MigratableEntity} from "@symbiotic/contracts/common/MigratableEntity.sol"; +import {VaultStorage} from "@symbiotic/contracts/vault/VaultStorage.sol"; + +import {IBaseDelegator} from "@symbiotic/interfaces/delegator/IBaseDelegator.sol"; +import {IBaseSlasher} from "@symbiotic/interfaces/slasher/IBaseSlasher.sol"; +import {IRegistry} from "@symbiotic/interfaces/common/IRegistry.sol"; +import {IVault} from "@symbiotic/interfaces/vault/IVault.sol"; +import {Checkpoints} from "@symbiotic/contracts/libraries/Checkpoints.sol"; +import {ERC4626Math} from "@symbiotic/contracts/libraries/ERC4626Math.sol"; + +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Time} from "@openzeppelin/contracts/utils/types/Time.sol"; + +contract iBTC_Vault is VaultStorage, MigratableEntity, AccessControlUpgradeable, IVault { + using Checkpoints for Checkpoints.Trace256; + using Math for uint256; + using SafeCast for uint256; + using SafeERC20 for IERC20; + + constructor( + address delegatorFactory, + address slasherFactory, + address vaultFactory + ) VaultStorage(delegatorFactory, slasherFactory) MigratableEntity(vaultFactory) {} + + /** + * @inheritdoc IVault + */ + function isInitialized() external view returns (bool) { + return isDelegatorInitialized && isSlasherInitialized; + } + + /** + * @inheritdoc IVault + */ + function totalStake() public view returns (uint256) { + uint256 epoch = currentEpoch(); + return activeStake() + withdrawals[epoch] + withdrawals[epoch + 1]; + } + + /** + * @inheritdoc IVault + */ + function activeBalanceOfAt(address account, uint48 timestamp, bytes calldata hints) public view returns (uint256) { + ActiveBalanceOfHints memory activeBalanceOfHints; + if (hints.length > 0) { + activeBalanceOfHints = abi.decode(hints, (ActiveBalanceOfHints)); + } + return ERC4626Math.previewRedeem( + activeSharesOfAt(account, timestamp, activeBalanceOfHints.activeSharesOfHint), + activeStakeAt(timestamp, activeBalanceOfHints.activeStakeHint), + activeSharesAt(timestamp, activeBalanceOfHints.activeSharesHint) + ); + } + + /** + * @inheritdoc IVault + */ + function activeBalanceOf( + address account + ) public view returns (uint256) { + return ERC4626Math.previewRedeem(activeSharesOf(account), activeStake(), activeShares()); + } + + /** + * @inheritdoc IVault + */ + function withdrawalsOf(uint256 epoch, address account) public view returns (uint256) { + return + ERC4626Math.previewRedeem(withdrawalSharesOf[epoch][account], withdrawals[epoch], withdrawalShares[epoch]); + } + + /** + * @inheritdoc IVault + */ + function slashableBalanceOf( + address account + ) external view returns (uint256) { + uint256 epoch = currentEpoch(); + return activeBalanceOf(account) + withdrawalsOf(epoch, account) + withdrawalsOf(epoch + 1, account); + } + + /** + * @inheritdoc IVault + */ + function deposit( + address onBehalfOf, + uint256 amount + ) public virtual nonReentrant returns (uint256 depositedAmount, uint256 mintedShares) { + if (onBehalfOf == address(0)) { + revert InvalidOnBehalfOf(); + } + + if (depositWhitelist && !isDepositorWhitelisted[msg.sender]) { + revert NotWhitelistedDepositor(); + } + + uint256 balanceBefore = IERC20(collateral).balanceOf(address(this)); + IERC20(collateral).safeTransferFrom(msg.sender, address(this), amount); + depositedAmount = IERC20(collateral).balanceOf(address(this)) - balanceBefore; + + if (depositedAmount == 0) { + revert InsufficientDeposit(); + } + + if (isDepositLimit && activeStake() + depositedAmount > depositLimit) { + revert DepositLimitReached(); + } + + uint256 activeStake_ = activeStake(); + uint256 activeShares_ = activeShares(); + + mintedShares = ERC4626Math.previewDeposit(depositedAmount, activeShares_, activeStake_); + + _activeStake.push(Time.timestamp(), activeStake_ + depositedAmount); + _activeShares.push(Time.timestamp(), activeShares_ + mintedShares); + _activeSharesOf[onBehalfOf].push(Time.timestamp(), activeSharesOf(onBehalfOf) + mintedShares); + + emit Deposit(msg.sender, onBehalfOf, depositedAmount, mintedShares); + } + + /** + * @inheritdoc IVault + */ + function withdraw( + address claimer, + uint256 amount + ) external nonReentrant returns (uint256 burnedShares, uint256 mintedShares) { + if (claimer == address(0)) { + revert InvalidClaimer(); + } + + if (amount == 0) { + revert InsufficientWithdrawal(); + } + + burnedShares = ERC4626Math.previewWithdraw(amount, activeShares(), activeStake()); + + if (burnedShares > activeSharesOf(msg.sender)) { + revert TooMuchWithdraw(); + } + + mintedShares = _withdraw(claimer, amount, burnedShares); + } + + /** + * @inheritdoc IVault + */ + function redeem( + address claimer, + uint256 shares + ) external nonReentrant returns (uint256 withdrawnAssets, uint256 mintedShares) { + if (claimer == address(0)) { + revert InvalidClaimer(); + } + + if (shares > activeSharesOf(msg.sender)) { + revert TooMuchRedeem(); + } + + withdrawnAssets = ERC4626Math.previewRedeem(shares, activeStake(), activeShares()); + + if (withdrawnAssets == 0) { + revert InsufficientRedemption(); + } + + mintedShares = _withdraw(claimer, withdrawnAssets, shares); + } + + /** + * @inheritdoc IVault + */ + function claim(address recipient, uint256 epoch) external nonReentrant returns (uint256 amount) { + if (recipient == address(0)) { + revert InvalidRecipient(); + } + + amount = _claim(epoch); + + IERC20(collateral).safeTransfer(recipient, amount); + + emit Claim(msg.sender, recipient, epoch, amount); + } + + /** + * @inheritdoc IVault + */ + function claimBatch(address recipient, uint256[] calldata epochs) external nonReentrant returns (uint256 amount) { + if (recipient == address(0)) { + revert InvalidRecipient(); + } + + uint256 length = epochs.length; + if (length == 0) { + revert InvalidLengthEpochs(); + } + + for (uint256 i; i < length; ++i) { + amount += _claim(epochs[i]); + } + + IERC20(collateral).safeTransfer(recipient, amount); + + emit ClaimBatch(msg.sender, recipient, epochs, amount); + } + + /** + * @inheritdoc IVault + */ + function onSlash(uint256 amount, uint48 captureTimestamp) external nonReentrant returns (uint256 slashedAmount) { + if (msg.sender != slasher) { + revert NotSlasher(); + } + + uint256 currentEpoch_ = currentEpoch(); + uint256 captureEpoch = epochAt(captureTimestamp); + if ((currentEpoch_ > 0 && captureEpoch < currentEpoch_ - 1) || captureEpoch > currentEpoch_) { + revert InvalidCaptureEpoch(); + } + + uint256 activeStake_ = activeStake(); + uint256 nextWithdrawals = withdrawals[currentEpoch_ + 1]; + if (captureEpoch == currentEpoch_) { + uint256 slashableStake = activeStake_ + nextWithdrawals; + slashedAmount = Math.min(amount, slashableStake); + if (slashedAmount > 0) { + uint256 activeSlashed = slashedAmount.mulDiv(activeStake_, slashableStake); + uint256 nextWithdrawalsSlashed = slashedAmount - activeSlashed; + + _activeStake.push(Time.timestamp(), activeStake_ - activeSlashed); + withdrawals[captureEpoch + 1] = nextWithdrawals - nextWithdrawalsSlashed; + } + } else { + uint256 withdrawals_ = withdrawals[currentEpoch_]; + uint256 slashableStake = activeStake_ + withdrawals_ + nextWithdrawals; + slashedAmount = Math.min(amount, slashableStake); + if (slashedAmount > 0) { + uint256 activeSlashed = slashedAmount.mulDiv(activeStake_, slashableStake); + uint256 nextWithdrawalsSlashed = slashedAmount.mulDiv(nextWithdrawals, slashableStake); + uint256 withdrawalsSlashed = slashedAmount - activeSlashed - nextWithdrawalsSlashed; + + if (withdrawals_ < withdrawalsSlashed) { + nextWithdrawalsSlashed += withdrawalsSlashed - withdrawals_; + withdrawalsSlashed = withdrawals_; + } + + _activeStake.push(Time.timestamp(), activeStake_ - activeSlashed); + withdrawals[currentEpoch_ + 1] = nextWithdrawals - nextWithdrawalsSlashed; + withdrawals[currentEpoch_] = withdrawals_ - withdrawalsSlashed; + } + } + + if (slashedAmount > 0) { + IERC20(collateral).safeTransfer(burner, slashedAmount); + } + + emit OnSlash(amount, captureTimestamp, slashedAmount); + } + + /** + * @inheritdoc IVault + */ + function setDepositWhitelist( + bool status + ) external nonReentrant onlyRole(DEPOSIT_WHITELIST_SET_ROLE) { + if (depositWhitelist == status) { + revert AlreadySet(); + } + + depositWhitelist = status; + + emit SetDepositWhitelist(status); + } + + /** + * @inheritdoc IVault + */ + function setDepositorWhitelistStatus( + address account, + bool status + ) external nonReentrant onlyRole(DEPOSITOR_WHITELIST_ROLE) { + if (account == address(0)) { + revert InvalidAccount(); + } + + if (isDepositorWhitelisted[account] == status) { + revert AlreadySet(); + } + + isDepositorWhitelisted[account] = status; + + emit SetDepositorWhitelistStatus(account, status); + } + + /** + * @inheritdoc IVault + */ + function setIsDepositLimit( + bool status + ) external nonReentrant onlyRole(IS_DEPOSIT_LIMIT_SET_ROLE) { + if (isDepositLimit == status) { + revert AlreadySet(); + } + + isDepositLimit = status; + + emit SetIsDepositLimit(status); + } + + /** + * @inheritdoc IVault + */ + function setDepositLimit( + uint256 limit + ) external nonReentrant onlyRole(DEPOSIT_LIMIT_SET_ROLE) { + if (depositLimit == limit) { + revert AlreadySet(); + } + + depositLimit = limit; + + emit SetDepositLimit(limit); + } + + function setDelegator( + address delegator_ + ) external nonReentrant { + if (isDelegatorInitialized) { + revert DelegatorAlreadyInitialized(); + } + + if (!IRegistry(DELEGATOR_FACTORY).isEntity(delegator_)) { + revert NotDelegator(); + } + + if (IBaseDelegator(delegator_).vault() != address(this)) { + revert InvalidDelegator(); + } + + delegator = delegator_; + + isDelegatorInitialized = true; + + emit SetDelegator(delegator_); + } + + function setSlasher( + address slasher_ + ) external nonReentrant { + if (isSlasherInitialized) { + revert SlasherAlreadyInitialized(); + } + + if (slasher_ != address(0)) { + if (!IRegistry(SLASHER_FACTORY).isEntity(slasher_)) { + revert NotSlasher(); + } + + if (IBaseSlasher(slasher_).vault() != address(this)) { + revert InvalidSlasher(); + } + + slasher = slasher_; + } + + isSlasherInitialized = true; + + emit SetSlasher(slasher_); + } + + function _withdraw( + address claimer, + uint256 withdrawnAssets, + uint256 burnedShares + ) internal virtual returns (uint256 mintedShares) { + _activeSharesOf[msg.sender].push(Time.timestamp(), activeSharesOf(msg.sender) - burnedShares); + _activeShares.push(Time.timestamp(), activeShares() - burnedShares); + _activeStake.push(Time.timestamp(), activeStake() - withdrawnAssets); + + uint256 epoch = currentEpoch() + 1; + uint256 withdrawals_ = withdrawals[epoch]; + uint256 withdrawalsShares_ = withdrawalShares[epoch]; + + mintedShares = ERC4626Math.previewDeposit(withdrawnAssets, withdrawalsShares_, withdrawals_); + + withdrawals[epoch] = withdrawals_ + withdrawnAssets; + withdrawalShares[epoch] = withdrawalsShares_ + mintedShares; + withdrawalSharesOf[epoch][claimer] += mintedShares; + + emit Withdraw(msg.sender, claimer, withdrawnAssets, burnedShares, mintedShares); + } + + function _claim( + uint256 epoch + ) internal returns (uint256 amount) { + if (epoch >= currentEpoch()) { + revert InvalidEpoch(); + } + + if (isWithdrawalsClaimed[epoch][msg.sender]) { + revert AlreadyClaimed(); + } + + amount = withdrawalsOf(epoch, msg.sender); + + if (amount == 0) { + revert InsufficientClaim(); + } + + isWithdrawalsClaimed[epoch][msg.sender] = true; + } + + function _initialize(uint64, address, bytes memory data) internal virtual override { + (InitParams memory params) = abi.decode(data, (InitParams)); + + if (params.collateral == address(0)) { + revert InvalidCollateral(); + } + + if (params.epochDuration == 0) { + revert InvalidEpochDuration(); + } + + if (params.defaultAdminRoleHolder == address(0)) { + if (params.depositWhitelistSetRoleHolder == address(0)) { + if (params.depositWhitelist) { + if (params.depositorWhitelistRoleHolder == address(0)) { + revert MissingRoles(); + } + } else if (params.depositorWhitelistRoleHolder != address(0)) { + revert MissingRoles(); + } + } + + if (params.isDepositLimitSetRoleHolder == address(0)) { + if (params.isDepositLimit) { + if (params.depositLimit == 0 && params.depositLimitSetRoleHolder == address(0)) { + revert MissingRoles(); + } + } else if (params.depositLimit != 0 || params.depositLimitSetRoleHolder != address(0)) { + revert MissingRoles(); + } + } + } + + collateral = params.collateral; + + burner = params.burner; + + epochDurationInit = Time.timestamp(); + epochDuration = params.epochDuration; + + depositWhitelist = params.depositWhitelist; + + isDepositLimit = params.isDepositLimit; + depositLimit = params.depositLimit; + + if (params.defaultAdminRoleHolder != address(0)) { + _grantRole(DEFAULT_ADMIN_ROLE, params.defaultAdminRoleHolder); + } + if (params.depositWhitelistSetRoleHolder != address(0)) { + _grantRole(DEPOSIT_WHITELIST_SET_ROLE, params.depositWhitelistSetRoleHolder); + } + if (params.depositorWhitelistRoleHolder != address(0)) { + _grantRole(DEPOSITOR_WHITELIST_ROLE, params.depositorWhitelistRoleHolder); + } + if (params.isDepositLimitSetRoleHolder != address(0)) { + _grantRole(IS_DEPOSIT_LIMIT_SET_ROLE, params.isDepositLimitSetRoleHolder); + } + if (params.depositLimitSetRoleHolder != address(0)) { + _grantRole(DEPOSIT_LIMIT_SET_ROLE, params.depositLimitSetRoleHolder); + } + } + + function _migrate(uint64, /* oldVersion */ uint64, /* newVersion */ bytes calldata /* data */ ) internal override { + revert(); + } +} diff --git a/src/iBTC_VaultConfigurator.sol b/src/iBTC_VaultConfigurator.sol new file mode 100644 index 0000000..d021596 --- /dev/null +++ b/src/iBTC_VaultConfigurator.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.25; + +import {BaseSlasher} from "@symbiotic/contracts/slasher/BaseSlasher.sol"; +import {DelegatorFactory} from "@symbiotic/contracts/DelegatorFactory.sol"; +import {SlasherFactory} from "@symbiotic/contracts/SlasherFactory.sol"; +import {VaultFactory} from "@symbiotic/contracts/VaultFactory.sol"; +import {IVaultConfigurator} from "@symbiotic/interfaces/IVaultConfigurator.sol"; + +import {iBTC_Vault} from "./iBTC_Vault.sol"; + +contract VaultConfigurator is IVaultConfigurator { + /** + * @inheritdoc IVaultConfigurator + */ + address public immutable VAULT_FACTORY; + + /** + * @inheritdoc IVaultConfigurator + */ + address public immutable DELEGATOR_FACTORY; + + /** + * @inheritdoc IVaultConfigurator + */ + address public immutable SLASHER_FACTORY; + + constructor(address vaultFactory, address delegatorFactory, address slasherFactory) { + VAULT_FACTORY = vaultFactory; + DELEGATOR_FACTORY = delegatorFactory; + SLASHER_FACTORY = slasherFactory; + } + + /** + * @inheritdoc IVaultConfigurator + */ + function create( + InitParams memory params + ) public returns (address vault, address delegator, address slasher) { + vault = VaultFactory(VAULT_FACTORY).create(params.version, params.owner, params.vaultParams); + + delegator = + DelegatorFactory(DELEGATOR_FACTORY).create(params.delegatorIndex, abi.encode(vault, params.delegatorParams)); + + if (params.withSlasher) { + slasher = + SlasherFactory(SLASHER_FACTORY).create(params.slasherIndex, abi.encode(vault, params.slasherParams)); + } + + iBTC_Vault(vault).setDelegator(delegator); + iBTC_Vault(vault).setSlasher(slasher); + } +} diff --git a/src/interfaces/IiBTC_Burner.sol b/src/interfaces/IiBTC_Burner.sol new file mode 100644 index 0000000..30f930e --- /dev/null +++ b/src/interfaces/IiBTC_Burner.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IUintRequests} from "@symbiotic-burners/contracts/common/UintRequests.sol"; + +interface IiBTC_Burner is IUintRequests { + error InsufficientWithdrawal(); + + /** + * @notice Emitted when a withdrawal is triggered. + * @param caller caller of the function + * @param firstRequestId first request ID that was created + * @param lastRequestId last request ID that was created + */ + event TriggerWithdrawal(address indexed caller, uint256 firstRequestId, uint256 lastRequestId); + + /** + * @notice Get an address of the collateral. + */ + function COLLATERAL() external view returns (address); + + /** + * @notice Get an address of the dlcBTC Exit contract. + */ + function iBTCTreasury() external view returns (address); + + /** + * @notice Trigger a withdrawal of BTC from the collateral's underlying asset. + * @param maxRequests maximum number of withdrawal requests to create + * @return firstRequestId first request ID that was created + * @return lastRequestId last request ID that was created + */ + function triggerWithdrawal( + uint256 maxRequests + ) external returns (uint256 firstRequestId, uint256 lastRequestId); +} diff --git a/src/interfaces/IiBTC_Treasury.sol b/src/interfaces/IiBTC_Treasury.sol new file mode 100644 index 0000000..b17740e --- /dev/null +++ b/src/interfaces/IiBTC_Treasury.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IiBTC_Treasury { + function getLastrequestIdCreated() external view returns (uint256); + + function withdrawRequestMaximum() external view returns (uint256); + + function withdrawRequestMinimum() external view returns (uint256); + + function processWithdrawals( + uint256 _lastTokenIdToProcess + ) external; + + function createWithdrawRequest( + uint256 amount + ) external; + + function finalizeWithdrawal( + uint256 tokenId + ) external; +} diff --git a/src/libraries/Counter.sol b/src/libraries/Counter.sol new file mode 100644 index 0000000..3631f01 --- /dev/null +++ b/src/libraries/Counter.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library Counters { + struct Counter { + uint256 _value; // Default: 0 + } + + function current( + Counter storage counter + ) internal view returns (uint256) { + return counter._value; + } + + function increment( + Counter storage counter + ) internal { + unchecked { + counter._value += 1; + } + } + + function decrement( + Counter storage counter + ) internal { + uint256 value = counter._value; + require(value > 0, "Counter: decrement overflow"); + unchecked { + counter._value = value - 1; + } + } + + function reset( + Counter storage counter + ) internal { + counter._value = 0; + } +} diff --git a/test/iBTC_Treasury_Burner.t.sol b/test/iBTC_Treasury_Burner.t.sol new file mode 100644 index 0000000..4da6eb6 --- /dev/null +++ b/test/iBTC_Treasury_Burner.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; +import "../src/iBTC_Treasury.sol"; +import "../src/iBTC_Burner.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MockERC20 is ERC20 { + constructor() ERC20("Mock Token", "MOCK") { + _mint(msg.sender, 1_000_000 ether); // Mint 1 million tokens to the deployer + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} + +contract iBTC_Treasury_BurnerTest is Test { + MockERC20 public mockCollateral; + iBTC_Treasury public treasury; + iBTC_Burner public burner; + + address public user = address(0x1234); + address public burnerDeployer = address(0x5678); + + uint256 public constant MAX_WITHDRAW_AMOUNT = 100 ether; + uint256 public constant MIN_WITHDRAW_AMOUNT = 10 ether; + + function setUp() public { + // Deploy Mock ERC20 Token + mockCollateral = new MockERC20(); + + // Deploy Treasury + treasury = new iBTC_Treasury(address(mockCollateral), MAX_WITHDRAW_AMOUNT, MIN_WITHDRAW_AMOUNT); + + // Deploy Burner + vm.startPrank(burnerDeployer); + burner = new iBTC_Burner(address(mockCollateral), address(treasury)); + vm.stopPrank(); + + // Distribute mock collateral to user and burner for testing + mockCollateral.mint(user, 500 ether); + mockCollateral.mint(address(burner), 500 ether); + } + + function testCreateWithdrawalRequest() public { + // User approves and creates a withdrawal request + vm.startPrank(user); + mockCollateral.approve(address(treasury), 50 ether); + + // Create withdrawal request + treasury.createWithdrawRequest(50 ether); + + // Verify request + assertEq(treasury.balanceOf(user), 1); // User should own one ERC721 token + assertEq(treasury.withdrawalRequests(1), 50 ether); // Verify request amount + vm.stopPrank(); + } + + function testTriggerWithdrawalFromBurner() public { + // Burner holds collateral and approves Treasury + uint256 burnerBalance = mockCollateral.balanceOf(address(burner)); + assertEq(burnerBalance, 500 ether); + + vm.startPrank(burnerDeployer); + + // Trigger a withdrawal using the burner + (uint256 firstRequestId, uint256 lastRequestId) = burner.triggerWithdrawal(2); + + // Verify request creation + assertEq(firstRequestId, 1); // First request ID should be 1 + assertEq(lastRequestId, 2); // Last request ID should be 2 + + uint256 finalRequestId = treasury.getLastrequestIdCreated(); + assertEq(finalRequestId, 2); // Verify the last created request ID matches + + vm.stopPrank(); + } + + function testFinalizeWithdrawal() public { + // User creates a withdrawal request + vm.startPrank(user); + mockCollateral.approve(address(treasury), 50 ether); + treasury.createWithdrawRequest(50 ether); + vm.stopPrank(); + + // User finalizes the withdrawal + vm.startPrank(address(this)); // 让 user 调用 finalizeWithdrawal + treasury.finalizeWithdrawal(1); + vm.stopPrank(); + + // Verify collateral transfer back to user + assertEq(mockCollateral.balanceOf(user), 500 ether); // 用户余额恢复到初始值 + assertEq(treasury.balanceOf(user), 0); // ERC721 token 应该已被销毁 + } + + function testBatchProcessWithdrawals() public { + assertEq(mockCollateral.balanceOf(user), 500 ether); + vm.startPrank(user); + mockCollateral.approve(address(treasury), 200 ether); + treasury.createWithdrawRequest(100 ether); + treasury.createWithdrawRequest(100 ether); + vm.stopPrank(); + + assertEq(mockCollateral.balanceOf(user), 300 ether); + vm.prank(address(this)); + treasury.processWithdrawals(2); + + uint256 finalUserBalance = mockCollateral.balanceOf(user); + console.log("Final user balance:", finalUserBalance); + assertEq(finalUserBalance, 500 ether); + + uint256 finalTreasuryBalance = mockCollateral.balanceOf(address(treasury)); + console.log("Final treasury balance:", finalTreasuryBalance); + assertEq(finalTreasuryBalance, 0 ether); + } +}