diff --git a/README.md b/README.md index 9265b45..fc86b0b 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,22 @@ -## Foundry +# dlc-symbiotic -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** +Repository for the dlc-link Symbiotic integration. -Foundry consists of: +## Usage -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. +You can start a local Sepolia fork using: -## Documentation +```shell +anvil --fork-url https://eth-sepolia.g.alchemy.com/v2/your-api-key +``` -https://book.getfoundry.sh/ +And deploy the NetworkMiddleware contract using: -## Usage +```shell +forge script SetupNetworkMiddleware --rpc-url http://localhost:8545 --broadcast -vvvv --private-key 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d +``` + +## Development ### Build diff --git a/src/SimpleMiddleware.sol b/src/SimpleMiddleware.sol index e563b5a..78d3ede 100644 --- a/src/SimpleMiddleware.sol +++ b/src/SimpleMiddleware.sol @@ -16,7 +16,7 @@ import {ISlasher} from "@symbiotic/interfaces/slasher/ISlasher.sol"; import {IVetoSlasher} from "@symbiotic/interfaces/slasher/IVetoSlasher.sol"; import {Subnetwork} from "@symbiotic/contracts/libraries/Subnetwork.sol"; -import {SimpleKeyRegistry32} from "./SimpleKeyRegistry32.sol"; +import {SimpleKeyRegistry32} from "./libraries/SimpleKeyRegistry32.sol"; import {MapWithTimeData} from "./libraries/MapWithTimeData.sol"; contract SimpleMiddleware is SimpleKeyRegistry32, Ownable { diff --git a/src/DLCLinkLibrary.sol b/src/dlc-core/DLCLinkLibrary.sol similarity index 100% rename from src/DLCLinkLibrary.sol rename to src/dlc-core/DLCLinkLibrary.sol diff --git a/src/DLCManager.sol b/src/dlc-core/DLCManager.sol similarity index 81% rename from src/DLCManager.sol rename to src/dlc-core/DLCManager.sol index da42b69..63d5477 100644 --- a/src/DLCManager.sol +++ b/src/dlc-core/DLCManager.sol @@ -7,19 +7,21 @@ pragma solidity 0.8.25; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts-upgradeable/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; -import "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./DLCLinkLibrary.sol"; -import "./DLCBTC.sol"; +import "./IBTC.sol"; + +import "../libraries/AggregatorV3Interface.sol"; /** * @author DLC.Link 2024 * @title DLCManager - * @dev This is the contract the Attestor Layer listens to. + * @dev This is the contract the Attestor Layer listens and writes to * @dev It is upgradable through the OpenZeppelin proxy pattern * @notice DLCManager is the main contract of the DLC.Link protocol. * @custom:contact eng@dlc.link @@ -28,7 +30,7 @@ import "./DLCBTC.sol"; contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, PausableUpgradeable { using DLCLink for DLCLink.DLC; using DLCLink for DLCLink.DLCStatus; - using SafeERC20 for DLCBTC; + using SafeERC20 for IBTC; //////////////////////////////////////////////////////////////// // STATE VARIABLES // @@ -48,7 +50,9 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, bytes32 public tssCommitment; string public attestorGroupPubKey; - DLCBTC public dlcBTC; // dlcBTC contract + // iBTC was historically called dlcBTC. + // Due the nature of upgradability, we have to keep the old name. + IBTC public dlcBTC; // iBTC contract. string public btcFeeRecipient; // BTC address to send fees to uint256 public minimumDeposit; // in sats uint256 public maximumDeposit; // in sats @@ -58,7 +62,11 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, mapping(address => bytes32[]) public userVaults; mapping(address => bool) private _whitelistedAddresses; - uint256[41] __gap; + bool public porEnabled; + AggregatorV3Interface public dlcBTCPoRFeed; + mapping(address => mapping(bytes32 => bool)) private _seenSigners; + uint256 public totalValueMinted; + uint256[38] __gap; //////////////////////////////////////////////////////////////// // ERRORS // @@ -66,6 +74,7 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, error NotDLCAdmin(); error IncompatibleRoles(); + error NoSignerRenouncement(); error ContractNotWhitelisted(); error NotCreatorContract(); error DLCNotFound(); @@ -79,6 +88,7 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, error NotEnoughSignatures(); error InvalidSigner(); error DuplicateSignature(); + error DuplicateSigner(address signer); error SignerNotApproved(address signer); error ClosingFundedVault(); @@ -91,6 +101,7 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, error InsufficientMintedBalance(uint256 minted, uint256 amount); error FeeRateOutOfBounds(uint256 feeRate); error UnderCollateralized(uint256 newValueLocked, uint256 valueMinted); + error NotEnoughReserves(uint256 reserves, uint256 amount); //////////////////////////////////////////////////////////////// // MODIFIERS // @@ -116,7 +127,7 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, modifier onlyVaultCreator( bytes32 _uuid ) { - if (dlcs[dlcIDsByUUID[_uuid]].creator != tx.origin) revert NotOwner(); + if (dlcs[dlcIDsByUUID[_uuid]].creator != msg.sender) revert NotOwner(); _; } @@ -124,7 +135,7 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, address defaultAdmin, address dlcAdminRole, uint16 threshold, - DLCBTC tokenContract, + IBTC tokenContract, string memory btcFeeRecipientToSet ) public initializer { __AccessControlDefaultAdminRules_init(2 days, defaultAdmin); @@ -143,6 +154,22 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, btcMintFeeRate = 12; // 0.12% BTC fee for now btcRedeemFeeRate = 15; // 0.15% BTC fee for now btcFeeRecipient = btcFeeRecipientToSet; + porEnabled = false; + totalValueMinted = 0; + } + + /** + * @notice Initialize total minted value tracking + * @dev This function is called once after the contract is upgraded with totalValueMinted tracking + */ + function initializeV2() public reinitializer(2) { + // Calculate initial total by iterating through existing vaults + uint256 total = 0; + for (uint256 i = 0; i < _index; i++) { + total += dlcs[i].valueMinted; + } + + totalValueMinted = total; } /// @custom:oz-upgrades-unsafe-allow constructor @@ -175,6 +202,8 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, event SetBtcFeeRecipient(string btcFeeRecipient); event SetWhitelistingEnabled(bool isWhitelistingEnabled); event TransferTokenContractOwnership(address newOwner); + event SetPorEnabled(bool enabled); + event SetDlcBTCPoRFeed(AggregatorV3Interface feed); //////////////////////////////////////////////////////////////// // INTERNAL FUNCTIONS // @@ -188,42 +217,72 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, * @notice Checks the 'signatures' of Attestors for a given 'message'. * @dev Recalculates the hash to make sure the signatures are for the same message. * @dev Uses OpenZeppelin's ECDSA library to recover the public keys from the signatures. - * @dev Signatures must be unique. + * @dev Signatures must be unique, from unique signers. * @param message Original message that was signed. * @param signatures Byte array of at least 'threshold' number of signatures. */ - function _attestorMultisigIsValid(bytes memory message, bytes[] memory signatures) internal view { + function _attestorMultisigIsValid(bytes memory message, bytes[] memory signatures) internal { if (signatures.length < _threshold) revert NotEnoughSignatures(); bytes32 prefixedMessageHash = MessageHashUtils.toEthSignedMessageHash(keccak256(message)); - if (_hasDuplicates(signatures)) revert DuplicateSignature(); - for (uint256 i = 0; i < signatures.length; i++) { address attestorPubKey = ECDSA.recover(prefixedMessageHash, signatures[i]); if (!hasRole(APPROVED_SIGNER, attestorPubKey)) { revert InvalidSigner(); } + _checkSignerUnique(attestorPubKey, prefixedMessageHash); } } + function _checkSignerUnique(address attestorPubKey, bytes32 messageHash) internal { + if (_seenSigners[attestorPubKey][messageHash]) { + revert DuplicateSigner(attestorPubKey); + } + _seenSigners[attestorPubKey][messageHash] = true; + } + /** - * @notice Checks for duplicate values in an array. - * @dev Used to check for duplicate signatures. - * @param signatures Array of signatures. - * @return bool True if there are duplicates, false otherwise. + * @notice Checks mint eligibility. + * @dev Checks if the amount is non-zero. + * @dev If PoR is disabled, returns true. + * @dev If PoR is enabled, checks if the new total value minted is within bounds. + * @dev If the PoR check fails, reverts with an error. + * @param amount dlcBTC to mint. + * @param currentTotalMinted total minted value in all vaults on this chain. + * @return bool whether a call to _mint should happen. */ - function _hasDuplicates( - bytes[] memory signatures - ) internal pure returns (bool) { - for (uint256 i = 0; i < signatures.length - 1; i++) { - for (uint256 j = i + 1; j < signatures.length; j++) { - if (keccak256(signatures[i]) == keccak256(signatures[j])) { - return true; - } - } + function _checkMint(uint256 amount, uint256 currentTotalMinted) internal view returns (bool) { + if (amount == 0) { + return false; + } + + uint256 proposedTotalValueMinted = currentTotalMinted + amount; + return _checkPoR(proposedTotalValueMinted); + } + + /** + * @notice Checks Proof of Reserves (PoR) eligibility. + * @dev If PoR is disabled, returns true. + * @dev If PoR is enabled, checks if the proposed total value minted is within bounds. + * @dev If the PoR check fails, reverts with an error. + * @param proposedTotalValueMinted proposed total minted value in all vaults on this chain. + * @return bool whether the proposed total value minted is within bounds. + */ + function _checkPoR( + uint256 proposedTotalValueMinted + ) internal view returns (bool) { + if (!porEnabled) { + return true; } - return false; + + (, int256 porValue,,,) = dlcBTCPoRFeed.latestRoundData(); + uint256 porValueUint = uint256(porValue); + + if (porValueUint < proposedTotalValueMinted) { + revert NotEnoughReserves(porValueUint, proposedTotalValueMinted); + } + return true; } function _mintTokens(address to, uint256 amount) internal { @@ -245,7 +304,7 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, * @return bytes32 uuid of the new vault/DLC */ function setupVault() external whenNotPaused onlyWhitelisted returns (bytes32) { - bytes32 _uuid = _generateUUID(tx.origin, _index); + bytes32 _uuid = _generateUUID(msg.sender, _index); dlcs[_index] = DLCLink.DLC({ uuid: _uuid, @@ -253,7 +312,7 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, valueLocked: 0, valueMinted: 0, timestamp: block.timestamp, - creator: tx.origin, + creator: msg.sender, status: DLCLink.DLCStatus.READY, fundingTxId: "", closingTxId: "", @@ -264,10 +323,10 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, taprootPubKey: "" }); - emit CreateDLC(_uuid, tx.origin, block.timestamp); + emit CreateDLC(_uuid, msg.sender, block.timestamp); dlcIDsByUUID[_uuid] = _index; - userVaults[tx.origin].push(_uuid); + userVaults[msg.sender].push(_uuid); _index++; return _uuid; @@ -319,7 +378,10 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, dlc.valueLocked = newValueLocked; dlc.valueMinted = newValueLocked; - _mintTokens(dlc.creator, amountToMint); + if (_checkMint(amountToMint, totalValueMinted)) { + totalValueMinted = totalValueMinted + amountToMint; + _mintTokens(dlc.creator, amountToMint); + } emit SetStatusFunded(uuid, btcTxId, dlc.creator, newValueLocked, amountToMint); } @@ -375,6 +437,7 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, } dlc.valueMinted -= amount; + totalValueMinted -= amount; _burnTokens(dlc.creator, amount); emit Withdraw(uuid, amount, msg.sender); } @@ -488,6 +551,13 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, } } + function renounceRole(bytes32 role, address account) public override { + if (account == msg.sender && role == APPROVED_SIGNER) { + revert NoSignerRenouncement(); + } + super.renounceRole(role, account); + } + function pauseContract() external onlyAdmin { _pause(); } @@ -600,4 +670,18 @@ contract DLCManager is Initializable, AccessControlDefaultAdminRulesUpgradeable, ) external onlyAdmin { dlcBTC.setBurner(burner); } + + function setPorEnabled( + bool enabled + ) external onlyAdmin { + porEnabled = enabled; + emit SetPorEnabled(enabled); + } + + function setDlcBTCPoRFeed( + AggregatorV3Interface feed + ) external onlyAdmin { + dlcBTCPoRFeed = feed; + emit SetDlcBTCPoRFeed(feed); + } } diff --git a/src/DLCBTC.sol b/src/dlc-core/IBTC.sol similarity index 64% rename from src/DLCBTC.sol rename to src/dlc-core/IBTC.sol index 8432c07..5066d6e 100644 --- a/src/DLCBTC.sol +++ b/src/dlc-core/IBTC.sol @@ -14,13 +14,14 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; /** * @author DLC.Link 2024 - * @title DLCBTC - * @notice The DLCBTC Token represents Bitcoin locked through the DLC.Link bridge - * @dev Owner is the TokenManager contract + * @title iBTC + * @notice The iBTC Token represents Bitcoin locked in self-custody by the DLC.Link protocol. + * @dev Owner is the DLCManager contract, which can mint/burn tokens + * @dev Minter/Burner rights are also given to CCIP token pools * @custom:contact eng@dlc.link * @custom:website https://www.dlc.link */ -contract DLCBTC is Initializable, ERC20Upgradeable, ERC20PermitUpgradeable, OwnableUpgradeable { +contract IBTC is Initializable, ERC20Upgradeable, ERC20PermitUpgradeable, OwnableUpgradeable { mapping(address => bool) public blacklisted; // deprecated. there is no blacklisting anymore address private _minter; address private _burner; @@ -37,9 +38,18 @@ contract DLCBTC is Initializable, ERC20Upgradeable, ERC20PermitUpgradeable, Owna } function initialize() public initializer { - __ERC20_init("dlcBTC", "dlcBTC"); + __ERC20_init("iBTC", "IBTC"); __Ownable_init(msg.sender); - __ERC20Permit_init("dlcBTC"); + __ERC20Permit_init("iBTC"); + } + + /** + * @notice Reinitializes the EIP712 domain separator + * @dev This function is used to reinitialize the EIP712 domain separator after a change in the name + * @dev Name changed from "dlcBTC" to "iBTC" + */ + function reinitializeEIP712() public reinitializer(2) { + __EIP712_init_unchained("iBTC", "1"); } modifier onlyOwnerOrCCIPMinter() { @@ -59,8 +69,12 @@ contract DLCBTC is Initializable, ERC20Upgradeable, ERC20PermitUpgradeable, Owna return 8; } + function name() public view virtual override returns (string memory) { + return "iBTC"; + } + function symbol() public view virtual override returns (string memory) { - return "dlcBTC"; + return "IBTC"; } function mint(address to, uint256 amount) external onlyOwnerOrCCIPMinter { @@ -71,16 +85,22 @@ contract DLCBTC is Initializable, ERC20Upgradeable, ERC20PermitUpgradeable, Owna _burn(from, amount); } - function burn(uint256 amount) external onlyCCIPBurner { + function burn( + uint256 amount + ) external onlyCCIPBurner { _burn(msg.sender, amount); } - function setMinter(address minter) external onlyOwner { + function setMinter( + address minter + ) external onlyOwner { _minter = minter; emit MinterSet(minter); } - function setBurner(address burner) external onlyOwner { + function setBurner( + address burner + ) external onlyOwner { _burner = burner; emit BurnerSet(burner); } diff --git a/src/libraries/AggregatorV3Interface.sol b/src/libraries/AggregatorV3Interface.sol new file mode 100644 index 0000000..2062677 --- /dev/null +++ b/src/libraries/AggregatorV3Interface.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// solhint-disable-next-line interface-starts-with-i +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + function getRoundData( + uint80 _roundId + ) + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); + + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound); +} diff --git a/src/SimpleKeyRegistry32.sol b/src/libraries/SimpleKeyRegistry32.sol similarity index 100% rename from src/SimpleKeyRegistry32.sol rename to src/libraries/SimpleKeyRegistry32.sol