Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/BTCDMinting.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;
/* solhint-disable private-vars-leading-underscore */
/* solhint-disable var-name-mixedcase */
import "./SingleAdminAccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "@openzeppelin/contracts/utils/structs/BitMaps.sol";
import "@openzeppelin/contracts/interfaces/IERC1271.sol";
import "./interfaces/IBTCD.sol";
import "./interfaces/IBTCDMinting.sol";
/**
* @title BTCD Minting
* @notice This contract mints and redeems BTCD
*/
contract BTCDMinting is IBTCDMinting, SingleAdminAccessControl, ReentrancyGuard, Pausable {
using SafeERC20 for IERC20;
using EnumerableSet for EnumerableSet.AddressSet;
using BitMaps for BitMaps.BitMap;
/* --------------- CONSTANTS --------------- */
/// @notice EIP712 domain
bytes32 private constant EIP712_DOMAIN =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
/// @notice order type
bytes32 public constant ORDER_TYPE = keccak256(
"Order(bytes32 order_id,uint8 order_type,uint120 expiry,uint256 nonce,address benefactor,address beneficiary,address collateral_asset,uint128 collateral_amount,uint128 btcd_amount)"
);
/// @notice role enabling to invoke mint
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
/// @notice role enabling to invoke redeem
bytes32 public constant REDEEMER_ROLE = keccak256("REDEEMER_ROLE");
/// @notice role enabling to transfer collateral to custody wallets
bytes32 public constant COLLATERAL_MANAGER_ROLE = keccak256("COLLATERAL_MANAGER_ROLE");
/// @notice role enabling to disable mint and redeem and remove minters and redeemers in an emergency
bytes32 public constant GATEKEEPER_ROLE = keccak256("GATEKEEPER_ROLE");
// Virtual role constants for business roles (not exposed in AccessControl but tracked in _accountRole)
bytes32 private constant VIRTUAL_BENEFACTOR_ROLE = keccak256("VIRTUAL_BENEFACTOR_ROLE");
bytes32 private constant VIRTUAL_CUSTODIAN_ROLE = keccak256("VIRTUAL_CUSTODIAN_ROLE");
/// @notice EIP712 domain hash
bytes32 private constant EIP712_DOMAIN_TYPEHASH = keccak256(abi.encodePacked(EIP712_DOMAIN));
/// @notice EIP 1271 magic value hash
bytes4 private constant EIP1271_MAGICVALUE = bytes4(keccak256("isValidSignature(bytes32,bytes)"));
/// @notice EIP712 name
bytes32 private constant EIP_712_NAME = keccak256("BTCDMinting");
/// @notice holds EIP712 revision
bytes32 private constant EIP712_REVISION = keccak256("1");
/// @notice required ratio for route
uint128 private constant ROUTE_REQUIRED_RATIO = 10_000;
/* --------------- STATE VARIABLES --------------- */
/// @notice btcd stablecoin
IBTCD public immutable btcd;
// @notice whitelisted benefactors
EnumerableSet.AddressSet private _whitelistedBenefactors;
// @notice approved beneficiaries for a given benefactor
mapping(address => EnumerableSet.AddressSet) private _approvedBeneficiariesPerBenefactor;
// @notice custodian addresses
EnumerableSet.AddressSet private _custodianAddresses;
/// @notice holds computable chain id
uint256 private immutable _chainId;
/// @notice holds computable domain separator
bytes32 private immutable DOMAIN_SEPARATOR;
/// @notice user deduplication
mapping(address => BitMaps.BitMap) private _usedNonces;
/// @notice For smart contracts to delegate signing to EOA address
mapping(address => mapping(address => DelegatedSignerStatus)) public delegatedSigner;
/// @notice global single block totals
GlobalConfig public globalConfig;
/// @notice running total BTCD minted/redeemed per single block
BlockTotals public totalPerBlock;
/// @notice total amount of collateral per asset that may be used to mint/redeem BTCD per single block.
mapping(address => BlockTotals) public totalPerBlockPerAsset;
/// @notice configurations per token asset
mapping(address => TokenConfig) public tokenConfig;
/* --------------- MODIFIERS --------------- */
/// @notice ensure that the per block global and asset specific mint limits are not exceeded for a mint order
modifier doesNotExceedPerBlockMintLimits(Order calldata _order) {
if (_order.order_type != OrderType.MINT) revert InvalidOrder();
_checkAndUpdateMintLimits(_order);
_;
}
/// @notice ensure that the per block global and asset specific redeem limits are not exceeded for a redemption order
modifier doesNotExceedPerBlockRedeemLimits(Order calldata _order) {
if (_order.order_type != OrderType.REDEEM) revert InvalidOrder();
_checkAndUpdateRedeemLimits(_order);
_;
}
/* --------------- CONSTRUCTOR --------------- */
constructor(
IBTCD _btcd,
address[] memory _assets,
TokenConfig[] memory _tokenConfig,
GlobalConfig memory _globalConfig,
address[] memory _custodians,
address _admin
) {
if (address(_btcd) == address(0)) revert InvalidBTCDAddress();
if (_assets.length == 0) revert NoAssetsProvided();
if (_tokenConfig.length != _assets.length) revert NoAssetsProvided();
if (_admin == address(0)) revert InvalidZeroAddress();
if (_globalConfig.globalMaxMintPerBlock == 0 || _globalConfig.globalMaxRedeemPerBlock == 0) {
revert InvalidGlobalConfig();
}
// Assert BTCD address
btcd = _btcd;
emit BTCDSet(address(_btcd));
// Set the global max BTCD mint/redeem limits
globalConfig = _globalConfig;
// Set the chainId
_chainId = block.chainid;
// Set the domain separator
DOMAIN_SEPARATOR = _computeDomainSeparator();
// Grant role to admin
_grantRole(DEFAULT_ADMIN_ROLE, _admin);
// Set the custodian addresses
for (uint128 j = 0; j < _custodians.length;) {
_addCustodianAddress(_custodians[j]);
unchecked {
++j;
}
}
// Set the max mint/redeem limits per block for each asset
for (uint128 k = 0; k < _tokenConfig.length;) {
_initializeTokenConfig(_assets[k], _tokenConfig[k]);
unchecked {
++k;
}
}
}
////////////////////////////////////////////////////////////
//// PRIVILEGED ROLE ACTIONS ///////////////////////////////
////////////////////////////////////////////////////////////
/* ----------- COLLATERAL MANAGER --------- */
/// @notice transfers an asset to a custody wallet
function transferToCustody(address wallet, address asset, uint128 amount)
external
nonReentrant
onlyRole(COLLATERAL_MANAGER_ROLE)
{
if (amount == 0) revert InvalidAmount();
if (wallet == address(0) || !_custodianAddresses.contains(wallet)) revert InvalidAddress();
IERC20(asset).safeTransfer(wallet, amount);
emit CustodyTransfer(wallet, asset, amount);
}
/* --------------- GATEKEEPER ------------- */
/// @notice Removes the minter role from an account, this can ONLY be executed by the gatekeeper role
/// @param minter The address to remove the minter role from
function removeMinterRole(address minter) external onlyRole(GATEKEEPER_ROLE) {
_revokeRole(MINTER_ROLE, minter);
}
/// @notice Removes the redeemer role from an account, this can ONLY be executed by the gatekeeper role
/// @param redeemer The address to remove the redeemer role from
function removeRedeemerRole(address redeemer) external onlyRole(GATEKEEPER_ROLE) {
_revokeRole(REDEEMER_ROLE, redeemer);
}
/// @notice Removes the collateral manager role from an account, this can ONLY be executed by the gatekeeper role
/// @param collateralManager The address to remove the collateralManager role from
function removeCollateralManagerRole(address collateralManager) external onlyRole(GATEKEEPER_ROLE) {
_revokeRole(COLLATERAL_MANAGER_ROLE, collateralManager);
}
/// @notice Disables mint and redeem operations, can ONLY be executed by the gatekeeper role
function disableMintAndRedeem() external onlyRole(GATEKEEPER_ROLE) {
_pause();
}
/* --------------- ADMIN ------------------ */
/// @notice Enables mint and redeem operations, can ONLY be executed by the admin role
function enableMintAndRedeem() external onlyRole(DEFAULT_ADMIN_ROLE) {
_unpause();
}
/// @notice Sets the overall, global maximum BTCD mint size per block
function setGlobalMaxMintPerBlock(uint128 _globalMaxMintPerBlock) external onlyRole(DEFAULT_ADMIN_ROLE) {
emit MaxMintPerBlockGlobalChanged(globalConfig.globalMaxMintPerBlock, _globalMaxMintPerBlock);
globalConfig.globalMaxMintPerBlock = _globalMaxMintPerBlock;
}
/// @notice Sets the overall, global maximum BTCD redeem size per block
function setGlobalMaxRedeemPerBlock(uint128 _globalMaxRedeemPerBlock) external onlyRole(DEFAULT_ADMIN_ROLE) {
emit MaxRedeemPerBlockGlobalChanged(globalConfig.globalMaxRedeemPerBlock, _globalMaxRedeemPerBlock);
globalConfig.globalMaxRedeemPerBlock = _globalMaxRedeemPerBlock;
}
/// @notice Sets the maximum amount of COLLATERAL ASSET that may be used for minting on a per block basis
/// @dev may be used to set to zero for pause of minting with a given asset
function setMaxMintPerBlock(uint128 _maxMintPerBlock, address asset) external onlyRole(DEFAULT_ADMIN_ROLE) {
_setMaxMintPerBlock(_maxMintPerBlock, asset);
}
/// @notice Sets the maximum amount of COLLATERAL ASSET that may be redeemed on a per block basis
/// @dev may be used to set to zero for pause of redeeming with a given asset
function setMaxRedeemPerBlock(uint128 _maxRedeemPerBlock, address asset) external onlyRole(DEFAULT_ADMIN_ROLE) {
_setMaxRedeemPerBlock(_maxRedeemPerBlock, asset);
}
/// @notice Adds an asset to the supported assets list
function addSupportedAsset(address asset, TokenConfig memory _tokenConfig) external onlyRole(DEFAULT_ADMIN_ROLE) {
_initializeTokenConfig(asset, _tokenConfig);
}
/// @notice Removes an asset from the supported assets list
function removeSupportedAsset(address asset) external onlyRole(DEFAULT_ADMIN_ROLE) {
_deleteTokenConfig(asset);
}
/// @notice Adds a benefactor address to the benefactor whitelist
function addWhitelistedBenefactor(address benefactor) public onlyRole(DEFAULT_ADMIN_ROLE) {
// Check for zero address first, before role conflict checking
if (benefactor == address(0)) {
revert InvalidBenefactorAddress();
}
// Check that the benefactor doesn't have any roles using unified system
if (_hasAnyAccessControlRole(benefactor)) {
revert RoleConflict();
}
if (!_whitelistedBenefactors.add(benefactor)) {
revert InvalidBenefactorAddress();
}
// Track in unified role system
_addRoleToAccount(benefactor, VIRTUAL_BENEFACTOR_ROLE);
emit BenefactorAdded(benefactor);
}
/// @notice Removes the benefactor address from the benefactor whitelist
function removeWhitelistedBenefactor(address benefactor) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (!_whitelistedBenefactors.remove(benefactor)) revert InvalidAddress();
// Remove from unified role system
_removeRoleFromAccount(benefactor, VIRTUAL_BENEFACTOR_ROLE);
emit BenefactorRemoved(benefactor);
}
/// @notice Adds an custodian to the supported custodians list.
function addCustodianAddress(address custodian) public onlyRole(DEFAULT_ADMIN_ROLE) {
_addCustodianAddress(custodian);
}
/// @notice Removes an custodian from the custodian address list
function removeCustodianAddress(address custodian) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (!_custodianAddresses.remove(custodian)) revert InvalidCustodianAddress();
// Remove from unified role system
_removeRoleFromAccount(custodian, VIRTUAL_CUSTODIAN_ROLE);
emit CustodianAddressRemoved(custodian);
}
/* --------------- MINTER ------------------ */
/**
* @notice Mint stablecoins from assets
* @param order struct containing order details and confirmation from server
* @param signature signature of the taker
*/
function mint(Order calldata order, Route calldata route, Signature calldata signature)
external
override
nonReentrant
onlyRole(MINTER_ROLE)
whenNotPaused
doesNotExceedPerBlockMintLimits(order)
{
verifyOrder(order, signature);
if (!verifyRoute(route)) revert InvalidRoute();
_deduplicateOrder(order.benefactor, order.nonce);
_transferCollateral(
order.collateral_amount, order.collateral_asset, order.benefactor, route.addresses, route.ratios
);
btcd.mint(order.beneficiary, order.btcd_amount);
emit Mint(
order.order_id,
order.benefactor,
order.beneficiary,
msg.sender,
order.collateral_asset,
order.collateral_amount,
order.btcd_amount
);
}
/* --------------- REDEEMER --------------- */
/**
* @notice Redeem stablecoins for assets
* @param order struct containing order details and confirmation from server
* @param signature signature of the taker
*/
function redeem(Order calldata order, Signature calldata signature)
external
override
nonReentrant
onlyRole(REDEEMER_ROLE)
whenNotPaused
doesNotExceedPerBlockRedeemLimits(order)
{
verifyOrder(order, signature);
_deduplicateOrder(order.benefactor, order.nonce);
btcd.burnFrom(order.benefactor, order.btcd_amount);
_transferToBeneficiary(order.beneficiary, order.collateral_asset, order.collateral_amount);
emit Redeem(
order.order_id,
order.benefactor,
order.beneficiary,
msg.sender,
order.collateral_asset,
order.collateral_amount,
order.btcd_amount
);
}
////////////////////////////////////////////////////////////
//// PUBLIC SETTERS/GETTERS/CHECKERS ///////////////////////
////////////////////////////////////////////////////////////
/* --------- BENEFACTOR SETTERS ------------*/
/// @notice Enables a benefactor to cancel an in-flight order through nonce consumption
function cancelOrder(uint256 nonce) external {
// ensure benefactor is authorized
if (!_whitelistedBenefactors.contains(msg.sender)) {
revert BenefactorNotWhitelisted();
}
_usedNonces[msg.sender].set(nonce); // one SSTORE, 256 nonces per word
}
/// @notice Enables smart contracts to delegate an address for signing
function setDelegatedSigner(address _delegateTo) external {
// Check that the delegated signer doesn't have any roles using unified system
if (_hasAnyAccessControlRole(_delegateTo)) {
revert RoleConflict();
}
if (!isWhitelistedBenefactor(msg.sender)) {
revert BenefactorNotWhitelisted();
}
if (delegatedSigner[msg.sender][_delegateTo] == DelegatedSignerStatus.PENDING) {
revert DelegationAlreadyPending();
}
if (delegatedSigner[msg.sender][_delegateTo] == DelegatedSignerStatus.CONFIRMED) {
revert DelegationAlreadyAccepted();
}
delegatedSigner[msg.sender][_delegateTo] = DelegatedSignerStatus.PENDING;
emit DelegatedSignerInitiated(msg.sender, _delegateTo);
}
/// @notice The delegated address to confirm delegation
function confirmDelegatedSigner(address _delegatedBy) external {
// Check that the confirming address doesn't have any roles using unified system
if (_hasAnyAccessControlRole(msg.sender)) {
revert RoleConflict();
}
if (delegatedSigner[_delegatedBy][msg.sender] == DelegatedSignerStatus.NOTINITIATED) {
revert DelegationNotInitiated();
}
if (delegatedSigner[_delegatedBy][msg.sender] == DelegatedSignerStatus.CONFIRMED) {
revert DelegationAlreadyAccepted();
}
delegatedSigner[_delegatedBy][msg.sender] = DelegatedSignerStatus.CONFIRMED;
emit DelegatedSignerAdded(_delegatedBy, msg.sender);
}
/// @notice Enables smart contracts to undelegate an address for signing
function removeDelegatedSigner(address _removedSigner) external {
if ( delegatedSigner[msg.sender][_removedSigner] == DelegatedSignerStatus.NOTINITIATED ) {
revert DelegationNotInitiated();
}
delegatedSigner[msg.sender][_removedSigner] = DelegatedSignerStatus.NOTINITIATED;
emit DelegatedSignerRemoved(msg.sender, _removedSigner);
}
/// @notice Adds a beneficiary address to the approved beneficiaries list.
/// @notice Only the benefactor can add or remove corresponding beneficiaries
/// @param beneficiary The beneficiary address
/// @param status The status of the beneficiary, true to be added, false to be removed.
function setApprovedBeneficiary(address beneficiary, bool status) public {
if (beneficiary == address(0)) revert InvalidZeroAddress();
if (status) {
// Check that the beneficiary doesn't have any roles using unified system
if (_hasAnyAccessControlRole(beneficiary)) {
revert RoleConflict();
}
if (!isWhitelistedBenefactor(msg.sender)) {
revert BenefactorNotWhitelisted();
}
if (!_approvedBeneficiariesPerBenefactor[msg.sender].add(beneficiary)) {
revert InvalidBeneficiaryAddress();
} else {
emit BeneficiaryAdded(msg.sender, beneficiary);
}
} else {
if (!_approvedBeneficiariesPerBenefactor[msg.sender].remove(beneficiary)) {
revert InvalidBeneficiaryAddress();
} else {
emit BeneficiaryRemoved(msg.sender, beneficiary);
}
}
}
/* --------------- GETTERS --------------- */
/// @notice returns whether an address is a whitelisted benefactor
function isWhitelistedBenefactor(address benefactor) public view returns (bool) {
return _whitelistedBenefactors.contains(benefactor);
}
/// @notice returns whether an address is a approved beneficiary per benefactor
function isApprovedBeneficiary(address benefactor, address beneficiary) public view returns (bool) {
return _approvedBeneficiariesPerBenefactor[benefactor].contains(beneficiary);
}
/// @notice Checks if an asset is supported.
function isSupportedAsset(address asset) external view returns (bool) {
return _isSupportedAsset(asset);
}
/// @notice returns whether an address is a custodian
function isCustodianAddress(address custodian) public view returns (bool) {
return _custodianAddresses.contains(custodian);
}
/// @notice Get the domain separator for the token
/// @dev Return cached value if chainId matches cache, otherwise recomputes separator, to prevent replay attack across forks
/// @return The domain separator of the token at current chain
function getDomainSeparator() public view returns (bytes32) {
if (block.chainid == _chainId) {
return DOMAIN_SEPARATOR;
}
return _computeDomainSeparator();
}
/// @notice hash an Order struct
function hashOrder(Order calldata order) public view override returns (bytes32) {
return ECDSA.toTypedDataHash(getDomainSeparator(), keccak256(encodeOrder(order)));
}
/// @notice encodes an Order struct
function encodeOrder(Order calldata order) public pure returns (bytes memory) {
return abi.encode(
ORDER_TYPE,
keccak256(bytes(order.order_id)),
order.order_type,
order.expiry,
order.nonce,
order.benefactor,
order.beneficiary,
order.collateral_asset,
order.collateral_amount,
order.btcd_amount
);
}
/* --------------- CHECKERS --------------- */
/// @notice assert validity of signed order
function verifyOrder(Order calldata order, Signature calldata signature)
public
view
override
returns (bytes32 taker_order_hash)
{
// no zero amounts
if (order.collateral_amount == 0 || order.btcd_amount == 0) revert InvalidAmount();
// no expired timestamp
if (block.timestamp > order.expiry) revert SignatureExpired();
// validate beneficiary
_verifyBeneficiary(order);
// validate signature
taker_order_hash = _verifySignature(order, signature);
}
/// @notice assert validity of route object per type
function verifyRoute(Route calldata route) public view override returns (bool) {
uint128 totalRatio = 0;
if (route.addresses.length != route.ratios.length) {
return false;
}
if (route.addresses.length == 0) {
return false;
}
for (uint256 i = 0; i < route.addresses.length;) {
if (route.addresses[i] == address(0) || route.ratios[i] == 0 ) {
return false;
}
if (!_custodianAddresses.contains(route.addresses[i]) ) {
return false;
}
totalRatio += route.ratios[i];
unchecked {
++i;
}
}
return (totalRatio == ROUTE_REQUIRED_RATIO);
}
/// @notice verify validity of nonce by checking its presence
function verifyNonce(address account, uint256 nonce) public view returns(bool) {
if (nonce == 0 || _usedNonces[account].get(nonce)) {
return false;
}
return true;
}
/* --------------- INTERNAL --------------- */
/// @notice Checks if an asset is supported.
function _isSupportedAsset(address asset) internal view returns (bool) {
if (asset == address(0) || asset == address(btcd)) {
return false;
}
return _isActiveTokenConfig(tokenConfig[asset]);
}
/// @notice Checks if a token config is active
function _isActiveTokenConfig(TokenConfig memory config) internal pure returns (bool) {
return config.isActive;
}
/// @notice deduplication of taker order
function _deduplicateOrder(address account, uint256 nonce) internal {
if (!verifyNonce(account, nonce) ) { // reverts on double use
revert InvalidNonce();
}
_usedNonces[account].set(nonce); // one SSTORE, 256 nonces per word
}
/// @notice verify signature of order
function _verifySignature(Order calldata order, Signature calldata signature) internal view returns (bytes32) {
// get order hash
bytes32 taker_order_hash = hashOrder(order);
// check EIP712 signature
if (signature.signature_type == SignatureType.EIP712) {
// recover signer
address signer = ECDSA.recover(taker_order_hash, signature.signature_bytes);
// if signer is not benefactor or an approved delegated signer for benefactor, revert
if (
!(
signer == order.benefactor
|| delegatedSigner[order.benefactor][signer] == DelegatedSignerStatus.CONFIRMED
)
) {
revert InvalidEIP712Signature();
}
// EIP712 signature ok
return taker_order_hash;
}
// check EIP1271 authorization
if (signature.signature_type == SignatureType.EIP1271) {
// call ERC1271 contract
if (
IERC1271(order.benefactor).isValidSignature(taker_order_hash, signature.signature_bytes)
!= EIP1271_MAGICVALUE
) {
revert InvalidEIP1271Signature();
}
// EIP1271 authorization ok
return taker_order_hash;
}
// signature type not known
revert UnknownSignatureType();
}
/// @notice verify beneficiary is valid per benefactor
function _verifyBeneficiary(Order calldata order) internal view {
// no zero address to beneficiary
if (order.beneficiary == address(0)) {
revert InvalidAddress();
}
// ensure benefactor is authorized
if (!_whitelistedBenefactors.contains(order.benefactor)) {
revert BenefactorNotWhitelisted();
}
// if benefactor is beneficiary, return ok
if (order.benefactor == order.beneficiary) {
return;
}
// if benefactor is not beneficiary, ensure beneficiary approved for benefactor
if (!_approvedBeneficiariesPerBenefactor[order.benefactor].contains(order.beneficiary)) {
revert BeneficiaryNotApproved();
}
}
/// @notice transfer supported asset to beneficiary address
function _transferToBeneficiary(address beneficiary, address asset, uint128 amount) internal {
IERC20(asset).safeTransfer(beneficiary, amount);
}
/// @notice transfer supported asset to array of custody addresses per defined ratio
function _transferCollateral(
uint128 amount,
address asset,
address benefactor,
address[] calldata addresses,
uint128[] calldata ratios
) internal {
IERC20 token = IERC20(asset);
uint256 len = addresses.length;
uint128 transferred = 0;
// send to every address except the last
for (uint256 i = 0; i + 1 < len;) {
uint128 slice = uint128((uint256(amount) * ratios[i]) / ROUTE_REQUIRED_RATIO);
transferred += slice;
if (slice == 0) {
revert InvalidRoute();
}
token.safeTransferFrom(benefactor, addresses[i], slice);
unchecked {
++i;
}
}
// final single transfer (carries any rounding residue)
uint128 finalAmount = amount - transferred;
if (finalAmount == 0) {
revert InvalidRoute();
}
token.safeTransferFrom(benefactor, addresses[len - 1], finalAmount);
}
/// @notice Compute the current domain separator
/// @return The domain separator for the token
function _computeDomainSeparator() internal view returns (bytes32) {
return keccak256(abi.encode(EIP712_DOMAIN, EIP_712_NAME, EIP712_REVISION, block.chainid, address(this)));
}
/// @notice Internal function that performs the logic of adding a custodian to the supported custodians list.
function _addCustodianAddress(address custodian) internal {
// Check that the custodian doesn't have any roles using unified system
if (_hasAnyAccessControlRole(custodian)) {
revert RoleConflict();
}
if (custodian == address(0) || custodian == address(btcd) || !_custodianAddresses.add(custodian)) {
revert InvalidCustodianAddress();
}
// Track in unified role system
_addRoleToAccount(custodian, VIRTUAL_CUSTODIAN_ROLE);
emit CustodianAddressAdded(custodian);
}
/* --------------- TOKEN CONFIG --------------- */
/// @notice Sets the max mintPerBlock limit for a given asset
function _setMaxMintPerBlock(uint128 _maxMintPerBlock, address asset) internal {
uint128 oldMaxMintPerBlock = tokenConfig[asset].maxMintPerBlock;
TokenConfig memory config = tokenConfig[asset];
config.maxMintPerBlock = _maxMintPerBlock;
_updateTokenConfig(asset, config);
emit MaxMintPerBlockChanged(oldMaxMintPerBlock, _maxMintPerBlock, asset);
}
/// @notice Sets the max redeemPerBlock limit for a given asset
function _setMaxRedeemPerBlock(uint128 _maxRedeemPerBlock, address asset) internal {
uint128 oldMaxRedeemPerBlock = tokenConfig[asset].maxRedeemPerBlock;
TokenConfig memory config = tokenConfig[asset];
config.maxRedeemPerBlock = _maxRedeemPerBlock;
_updateTokenConfig(asset, config);
emit MaxRedeemPerBlockChanged(oldMaxRedeemPerBlock, _maxRedeemPerBlock, asset);
}
/// @notice Sets the token config for a given asset
/// @dev enforces that the asset is not already active and at least one of mint or redeem is non-zero
function _initializeTokenConfig(address asset, TokenConfig memory _tokenConfig) internal {
if (tokenConfig[asset].isActive) {
revert InvalidAssetAddress();
}
if (_tokenConfig.maxMintPerBlock == 0) {
revert InvalidAmount();
}
_assignTokenConfig(asset, _tokenConfig);
emit AssetAdded(asset);
}
/// @notice Updates the token config for a given asset
/// @dev enforces that the asset is already active
function _updateTokenConfig(address asset, TokenConfig memory _tokenConfig) internal {
if (! _isSupportedAsset(asset)) {
revert InvalidAssetAddress();
}
_assignTokenConfig(asset, _tokenConfig);
}
/// @notice Assigns the token config for a given asset
/// @dev should only be called by _setTokenConfig or _updateTokenConfig
function _assignTokenConfig(address asset, TokenConfig memory _tokenConfig) internal {
if (asset == address(0) || asset == address(btcd)) {
revert InvalidAssetAddress();
}
if (!_isActiveTokenConfig(_tokenConfig)) { // NOTICE THIS CHECK IS AGAINST THE LOCAL VARIABLE, NOT THE STORAGE
revert InvalidTokenConfig();
}
tokenConfig[asset] = _tokenConfig;
}
function _deleteTokenConfig(address asset) internal {
if (!_isSupportedAsset(asset)) revert InvalidAssetAddress();
delete tokenConfig[asset];
emit AssetRemoved(asset);
}
/* --------------- Limits --------------- */
/// @notice Internal function to check and update mint limits
function _checkAndUpdateMintLimits(Order calldata _order) internal {
TokenConfig memory _config = tokenConfig[_order.collateral_asset];
if (!_isActiveTokenConfig(_config)) revert UnsupportedAsset();
_checkGlobalMintLimits(_order.btcd_amount);
_checkAssetMintLimits(_order.collateral_asset, _order.collateral_amount, _config.maxMintPerBlock);
}
/// @notice Internal function to check and update redeem limits
function _checkAndUpdateRedeemLimits(Order calldata _order) internal {
TokenConfig memory _config = tokenConfig[_order.collateral_asset];
if (!_isActiveTokenConfig(_config)) revert UnsupportedAsset();
_checkGlobalRedeemLimits(_order.btcd_amount);
_checkAssetRedeemLimits(_order.collateral_asset, _order.collateral_amount, _config.maxRedeemPerBlock);
}
/// @notice Check global mint limits and update storage
function _checkGlobalMintLimits(uint128 btcdAmount) internal {
BlockTotals memory globalTotals = totalPerBlock;
GlobalConfig memory _globalConfig = globalConfig;
bool isNewBlock = globalTotals.lastBlockNumber < block.number;
uint128 currentGlobalMinted = isNewBlock ? 0 : globalTotals.mintedPerBlock;
if (currentGlobalMinted + btcdAmount > _globalConfig.globalMaxMintPerBlock) {
revert GlobalMaxMintPerBlockExceeded();
}
if (isNewBlock) {
totalPerBlock.lastBlockNumber = block.number;
totalPerBlock.mintedPerBlock = btcdAmount;
totalPerBlock.redeemedPerBlock = 0;
} else {
totalPerBlock.mintedPerBlock = currentGlobalMinted + btcdAmount;
}
}
/// @notice Check global redeem limits and update storage
function _checkGlobalRedeemLimits(uint128 btcdAmount) internal {
BlockTotals memory globalTotals = totalPerBlock;
GlobalConfig memory _globalConfig = globalConfig;
bool isNewBlock = globalTotals.lastBlockNumber < block.number;
uint128 currentGlobalRedeemed = isNewBlock ? 0 : globalTotals.redeemedPerBlock;
if (currentGlobalRedeemed + btcdAmount > _globalConfig.globalMaxRedeemPerBlock) {
revert GlobalMaxRedeemPerBlockExceeded();
}
if (isNewBlock) {
totalPerBlock.lastBlockNumber = block.number;
totalPerBlock.redeemedPerBlock = btcdAmount;
totalPerBlock.mintedPerBlock = 0;
} else {
totalPerBlock.redeemedPerBlock = currentGlobalRedeemed + btcdAmount;
}
}
/// @notice Check asset-specific mint limits and update storage
function _checkAssetMintLimits(address asset, uint128 collateralAmount, uint128 maxMintPerBlock) internal {
BlockTotals memory assetTotals = totalPerBlockPerAsset[asset];
bool isNewBlockForAsset = assetTotals.lastBlockNumber < block.number;
uint128 currentAssetMinted = isNewBlockForAsset ? 0 : assetTotals.mintedPerBlock;
if (currentAssetMinted + collateralAmount > maxMintPerBlock) {
revert MaxMintPerBlockExceeded();
}
if (isNewBlockForAsset) {
totalPerBlockPerAsset[asset].lastBlockNumber = block.number;
totalPerBlockPerAsset[asset].mintedPerBlock = collateralAmount;
totalPerBlockPerAsset[asset].redeemedPerBlock = 0;
} else {
totalPerBlockPerAsset[asset].mintedPerBlock = currentAssetMinted + collateralAmount;
}
}
/// @notice Check asset-specific redeem limits and update storage
function _checkAssetRedeemLimits(address asset, uint128 collateralAmount, uint128 maxRedeemPerBlock) internal {
BlockTotals memory assetTotals = totalPerBlockPerAsset[asset];
bool isNewBlockForAsset = assetTotals.lastBlockNumber < block.number;
uint128 currentAssetRedeemed = isNewBlockForAsset ? 0 : assetTotals.redeemedPerBlock;
if (currentAssetRedeemed + collateralAmount > maxRedeemPerBlock) {
revert MaxRedeemPerBlockExceeded();
}
if (isNewBlockForAsset) {
totalPerBlockPerAsset[asset].lastBlockNumber = block.number;
totalPerBlockPerAsset[asset].redeemedPerBlock = collateralAmount;
totalPerBlockPerAsset[asset].mintedPerBlock = 0;
} else {
totalPerBlockPerAsset[asset].redeemedPerBlock = currentAssetRedeemed + collateralAmount;
}
}
}
"
},
"src/SingleAdminAccessControl.sol": {
"content": "// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/interfaces/IERC5313.sol";
import "./interfaces/ISingleAdminAccessControl.sol";
/**
* @title SingleAdminAccessControl
* @notice SingleAdminAccessControl is a contract that provides a single admin role
* @notice This contract is a simplified alternative to OpenZeppelin's AccessControlDefaultAdminRules
*/
abstract contract SingleAdminAccessControl is IERC5313, ISingleAdminAccessControl, AccessControl {
address private _currentDefaultAdmin;
address private _pendingDefaultAdmin;
// Track the single role for each account (since no account can have multiple roles)
mapping(address => bytes32) private _accountRole;
modifier notAdmin(bytes32 role) {
if (role == DEFAULT_ADMIN_ROLE) revert InvalidAdminChange();
_;
}
/// @notice Transfer the admin role to a new address
/// @notice This can ONLY be executed by the current admin
/// @param newAdmin address
function transferAdmin(address newAdmin) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (newAdmin == msg.sender) revert InvalidAdminChange();
// Check that the new admin doesn't already have any roles
_checkRoleConflictsForAdmin(newAdmin);
_pendingDefaultAdmin = newAdmin;
emit AdminTransferRequested(_currentDefaultAdmin, newAdmin);
}
function acceptAdmin() external {
if (msg.sender != _pendingDefaultAdmin) revert NotPendingAdmin();
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
/// @notice grant a role
/// @notice can only be executed by the current single admin
/// @notice admin role cannot be granted externally
/// @param role bytes32
/// @param account address
function grantRole(bytes32 role, address account) public override onlyRole(DEFAULT_ADMIN_ROLE) notAdmin(role) {
_grantRole(role, account);
}
/// @notice revoke a role
/// @notice can only be executed by the current admin
/// @notice admin role cannot be revoked
/// @param role bytes32
/// @param account address
function revokeRole(bytes32 role, address account) public override onlyRole(DEFAULT_ADMIN_ROLE) notAdmin(role) {
_revokeRole(role, account);
}
/// @notice renounce the role of msg.sender
/// @notice admin role cannot be renounced
/// @param role bytes32
/// @param account address
function renounceRole(bytes32 role, address account) public virtual override notAdmin(role) {
if (hasRole(role, account)) {
_removeRoleFromAccount(account, role);
}
super.renounceRole(role, account);
}
/**
* @dev See {IERC5313-owner}.
*/
function owner() public view virtual returns (address) {
return _currentDefaultAdmin;
}
/**
* @notice Get the pending admin address
* @return The pending admin address, or address(0) if none
*/
function pendingAdmin() public view returns (address) {
return _pendingDefaultAdmin;
}
/**
* @notice Internal function to check if an account has any AccessControl role
* @param account The account to check
* @return true if the account has any role, false otherwise
*/
function _hasAnyAccessControlRole(address account) internal view returns (bool) {
// Check if account has any tracked role
if (_accountRole[account] != bytes32(0)) {
return true;
}
// Check if account is pending admin
if (account == _pendingDefaultAdmin) {
return true;
}
// Check if account is current admin
if (account == _currentDefaultAdmin) {
return true;
}
return false;
}
/**
* @notice no way to change admin without removing old admin first
*/
function _grantRole(bytes32 role, address account) internal override {
if (role == DEFAULT_ADMIN_ROLE) {
// Check for role conflicts even for admin role - no address should have multiple roles
_checkRoleConflictsForAdmin(account);
emit AdminTransferred(_currentDefaultAdmin, account);
_revokeRole(DEFAULT_ADMIN_ROLE, _currentDefaultAdmin);
_currentDefaultAdmin = account;
delete _pendingDefaultAdmin;
} else {
// Check for role conflicts - ensure no address has multiple non-admin roles
_checkRoleConflicts(role, account);
// Track the role for this account
_addRoleToAccount(account, role);
}
super._grantRole(role, account);
}
/**
* @notice Override _revokeRole to clean up role tracking
*/
function _revokeRole(bytes32 role, address account) internal override {
if (hasRole(role, account)) {
_removeRoleFromAccount(account, role);
}
super._revokeRole(role, account);
}
/**
* @notice Check that granting a role doesn't create conflicts with existing roles
* @param account The account receiving the role
*/
function _checkRoleConflicts(bytes32, address account) private view {
// Use the consolidated role checking logic
if (_hasAnyAccessControlRole(account)) {
revert RoleConflict();
}
}
/**
* @notice Check that granting admin role doesn't create conflicts with existing roles
* @param account The account receiving the admin role
*/
function _checkRoleConflictsForAdmin(address account) private view {
// Check if the account already has any role (excluding admin checks since this IS for admin)
if (_accountRole[account] != bytes32(0)) {
// Account already has a role, so we can't grant admin role
revert RoleConflict();
}
}
/**
* @notice Add a role to an account's role tracking
* @param account The account
* @param role The role to add
*/
function _addRoleToAccount(address account, bytes32 role) internal {
_accountRole[account] = role;
}
/**
* @notice Remove a role from an account's role tracking
* @param account The account
* @param role The role to remove
*/
function _removeRoleFromAccount(address account, bytes32 role) internal {
if (_accountRole[account] == role) {
delete _accountRole[account];
}
}
}
"
},
"lib/openzeppelin-contracts/contracts/security/ReentrancyGuard.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)
pragma solidity ^0.8.0;
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be _NOT_ENTERED
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == _ENTERED;
}
}
"
},
"lib/openzeppelin-contracts/contracts/security/Pausable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol)
pragma solidity ^0.8.0;
import "../utils/Context.sol";
/**
* @dev Contract module which allows children to implement an emergency stop
* mechanism that can be triggered by an authorized account.
*
* This module is used through inheritance. It will make available the
* modifiers `whenNotPaused` and `whenPaused`, which can be applied to
* the functions of your contract. Note that they will not be pausable by
* simply including this module, only once the modifiers are put in place.
*/
abstract contract Pausable is Context {
/**
* @dev Emitted when the pause is triggered by `account`.
*/
event Paused(address account);
/**
* @dev Emitted when the pause is lifted by `account`.
*/
event Unpaused(address account);
bool private _paused;
/**
* @dev Initializes the contract in unpaused state.
*/
constructor() {
_paused = false;
}
/**
* @dev Modifier to make a function callable only when the contract is not paused.
*
* Requirements:
*
* - The contract must not be paused.
*/
modifier whenNotPaused() {
_requireNotPaused();
_;
}
/**
* @dev Modifier to make a function callable only when the contract is paused.
*
* Requirements:
*
* - The contract must be paused.
*/
modifier whenPaused() {
_requirePaused();
_;
}
/**
* @dev Returns true if the contract is paused, and false otherwise.
*/
function paused() public view virtual returns (bool) {
return _paused;
}
/**
* @dev Throws if the contract is paused.
*/
function _requireNotPaused() internal view virtual {
require(!paused(), "Pausable: paused");
}
/**
* @dev Throws if the contract is not paused.
*/
function _requirePaused() internal view virtual {
require(paused(), "Pausable: not paused");
}
/**
* @dev Triggers stopped state.
*
* Requirements:
*
* - The contract must not be paused.
*/
function _pause() internal virtual whenNotPaused {
_paused = true;
emit Paused(_msgSender());
}
/**
* @dev Returns to normal state.
*
* Requirements:
*
* - The contract must be paused.
*/
function _unpause() internal virtual whenPaused {
_paused = false;
emit Unpaused(_msgSender());
}
}
"
},
"lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.3) (token/ERC20/utils/SafeERC20.sol)
pragma solidity ^0.8.0;
import "../IERC20.sol";
import "../extensions/IERC20Permit.sol";
import "../../../utils/Address.sol";
/**
* @title SafeERC20
* @dev Wrappers around ERC20 operations that throw on failure (when the token
* contract returns false). Tokens that return no value (and instead revert or
* throw on failure) are also supported, non-reverting calls are assumed to be
* successful.
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
*/
library SafeERC20 {
using Address for address;
/**
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeTransfer(IERC20 token, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}
/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful.
*/
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
_callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
}
/**
* @dev Deprecated. This function has issues similar to the ones found in
* {IERC20-approve}, and its usage is discouraged.
*
* Whenever possible, use {safeIncreaseAllowance} and
* {safeDecreaseAllowance} instead.
*/
function safeApprove(IERC20 token, address spender, uint256 value) internal {
// safeApprove should only be called when setting an initial allowance,
// or when resetting it to zero. To increase and decrease it, use
// 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
require(
(value == 0) || (token.allowance(address(this), spender) == 0),
"SafeERC20: approve from non-zero to non-zero allowance"
);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
}
/**
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
uint256 oldAllowance = token.allowance(address(this), spender);
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance + value));
}
/**
* @dev Decrease the calling contract's allowance toward `spender` by `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful.
*/
function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal {
unchecked {
uint256 oldAllowance = token.allowance(address(this), spender);
require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, oldAllowance - value));
}
}
/**
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value,
* non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval
* to be set to zero before setting it to a non-zero value, such as USDT.
*/
function forceApprove(IERC20 token, address spender, uint256 value) internal {
bytes memory approvalCall = abi.encodeWithSelector(token.approve.selector, spender, value);
if (!_callOptionalReturnBool(token, approvalCall)) {
_callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, 0));
_callOptionalReturn(token, approvalCall);
}
}
/**
* @dev Use a ERC-2612 signature to set the `owner` approval toward `spender` on `token`.
* Revert on invalid signature.
*/
function safePermit(
IERC20Permit token,
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) internal {
uint256 nonceBefore = token.nonces(owner);
token.permit(owner, spender, value, deadline, v, r, s);
uint256 nonceAfter = token.nonces(owner);
require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed");
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*/
function _callOptionalReturn(IERC20 token, bytes memory data) private {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
// the target address contains contract code and also asserts for success in the low-level call.
bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
require(returndata.length == 0 || abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
}
/**
* @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
* on the return value: the return value is optional (but if data is returned, it must not be false).
* @param token The token targeted by the call.
* @param data The call data (encoded using abi.encode or one of its variants).
*
* This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead.
*/
function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
// We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
// we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false
// and not revert is the subcall reverts.
(bool success, bytes memory returndata) = address(token).call(data);
return
success && (returndata.length == 0 || abi.decode(returndata, (bool))) && Address.isContract(address(token));
}
}
"
},
"lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/ECDSA.sol)
pragma solidity ^0.8.0;
import "../Strings.sol";
/**
* @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations.
*
* These functions can be used to verify that a message was signed by the holder
* of the private keys of a given address.
*/
library ECDSA {
enum RecoverError {
NoError,
InvalidSignature,
InvalidSignatureLength,
InvalidSignatureS,
InvalidSignatureV // Deprecated in v4.8
}
function _throwError(RecoverError error) private pure {
if (error == RecoverError.NoError) {
return; // no error: do nothing
} else if (error == RecoverError.InvalidSignature) {
revert("ECDSA: invalid signature");
} else if (error == RecoverError.InvalidSignatureLength) {
revert("ECDSA: invalid signature length");
} else if (error == RecoverError.InvalidSignatureS) {
revert("ECDSA: invalid signature 's' value");
}
}
/**
* @dev Returns the address that signed a hashed message (`hash`) with
* `signature` or error string. This address can then be used for verification purposes.
*
* The `ecrecover` EVM opcode allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {toEthSignedMessageHash} on it.
*
* Documentation for signature generation:
* - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js]
* - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers]
*
* _Available since v4.3._
*/
function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) {
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
/// @solidity memory-safe-assembly
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
return tryRecover(hash, v, r, s);
} else {
return (address(0), RecoverError.InvalidSignatureLength);
}
}
/**
* @dev Returns the address that signed a hashed message (`hash`) with
* `signature`. This address can then be used for verification purposes.
*
* The `ecrecover` EVM opcode allows for malleable (non-unique) signatures:
* this function rejects them by requiring the `s` value to be in the lower
* half order, and the `v` value to be either 27 or 28.
*
* IMPORTANT: `hash` _must_ be the result of a hash operation for the
* verification to be secure: it is possible to craft signatures that
* recover to arbitrary addresses for non-hashed data. A safe way to ensure
* this is by receiving a hash of the original message (which may otherwise
* be too long), and then calling {toEthSignedMessageHash} on it.
*/
function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, signature);
_throwError(error);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately.
*
* See https://eips.ethereum.org/EIPS/eip-2098[EIP-2098 short signatures]
*
* _Available since v4.3._
*/
function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError) {
bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
uint8 v = uint8((uint256(vs) >> 255) + 27);
return tryRecover(hash, v, r, s);
}
/**
* @dev Overload of {ECDSA-recover} that receives the `r and `vs` short-signature fields separately.
*
* _Available since v4.2._
*/
function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, r, vs);
_throwError(error);
return recovered;
}
/**
* @dev Overload of {ECDSA-tryRecover} that receives the `v`,
* `r` and `s` signature fields separately.
*
* _Available since v4.3._
*/
function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address, RecoverError) {
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most
// signatures from current libraries generate a unique signature with an s-value in the lower half order.
//
// If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value
// with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or
// vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept
// these malleable signatures as well.
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return (address(0), RecoverError.InvalidSignatureS);
}
// If the signature is valid (and not malleable), return the signer address
address signer = ecrecover(hash, v, r, s);
if (signer == address(0)) {
return (address(0), RecoverError.InvalidSignature);
}
return (signer, RecoverError.NoError);
}
/**
* @dev Overload of {ECDSA-recover} that receives the `v`,
* `r` and `s` signature fields separately.
*/
function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, v, r, s);
_throwError(error);
return recovered;
}
/**
* @dev Returns an Ethereum Signed Message, created from a `hash`. This
* produces hash corresponding to the one signed with the
* https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
* JSON-RPC method as part of EIP-191.
*
* See {recover}.
*/
function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 message) {
// 32 is the length in bytes of hash,
// enforced by the type signature above
/// @solidity memory-safe-assembly
assembly {
mstore(0x00, "\x19Ethereum Signed Message:\
32")
mstore(0x1c, hash)
message := keccak256(0x00, 0x3c)
}
}
/**
* @dev Returns an Ethereum Signed Message, created from `s`. This
* produces hash corresponding to the one signed with the
* https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
* JSON-RPC method as part of EIP-191.
*
* See {recover}.
*/
function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\
", Strings.toString(s.length), s));
}
/**
* @dev Returns an Ethereum Signed Typed Data, created from a
* `domainSeparator` and a `structHash`. This produces hash corresponding
* to the one signed with the
* https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`]
* JSON-RPC method as part of EIP-712.
*
* See {recover}.
*/
function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 data) {
/// @solidity memory-safe-assembly
assembly {
let ptr := mload(0x40)
mstore(ptr, "\x19\x01")
mstore(add(ptr, 0x02), domainSeparator)
mstore(add(ptr, 0x22), structHash)
data := keccak256(ptr, 0x42)
}
}
/**
* @dev Returns an Ethereum Signed Data with
Submitted on: 2025-10-29 20:27:47
Comments
Log in to comment.
No comments yet.