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": {
"contracts/2025.09.22 T76/AssetVault.sol": {
"content": "// SPDX-License-Identifier: MIT\r
pragma solidity ^0.8.30;\r
\r
import "@openzeppelin/contracts/access/Ownable.sol";\r
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";\r
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";\r
\r
contract AssetVault is Ownable, ReentrancyGuard {\r
\r
string public constant VERSION = "1.0.1";\r
\r
// Error definitions\r
error InvalidZeroAddress();\r
error NotAuthorized(address user, uint8 requiredRole);\r
error InvalidZeroAmount();\r
error EmptyTransactionHash();\r
error UnauthorizedCaller(address caller);\r
error InsufficientAssetBalance(uint256 requiredETH, uint256 availableETH, uint256 requiredGPT, uint256 availableGPT, uint256 requiredBTC, uint256 availableBTC);\r
error InvalidRequestId(uint256 requestId);\r
error RequestAlreadyProcessed(uint256 requestId);\r
error InvalidParameters();\r
error MissingTransactionHash(string assetType);\r
error TransactionHashAlreadyUsed(string transactionHash);\r
error ZeroWithdrawalAmount();\r
error InsufficientBalance(string assetType);\r
error InvalidETHReceiverAddress();\r
error InvalidGPTReceiverAddress();\r
error InvalidBTCReceiverAddress();\r
error AddressAlreadyExists();\r
error AddressNotFound();\r
error InvalidAssetType();\r
\r
uint256 private constant MAX_REQUEST_LIMIT = 100;\r
\r
// Minimum amount thresholds\r
uint256 public minEthTxAmount = 1000000000000000; // 0.001 ETH (wei, 10^-18)\r
uint256 public minGptTxAmount = 1000000000000000; // 0.001 GPT (in 10^-18 units)\r
uint256 public minBtcTxAmount = 6000; // 0.00006 BTC (in satoshi)\r
\r
// Role constants - Using bitmasks\r
uint8 private constant ROLE_RECORDER = 1;\r
uint8 private constant ROLE_PROCESSOR = 2;\r
\r
// Asset type enumeration\r
enum AssetType {ETH, GPT, BTC}\r
\r
// Locked asset structure\r
struct LockedAssets {\r
uint256 totalETH; // Total ETH amount (in wei)\r
uint256 totalGPT; // Total GPT amount (in 10^-18 units)\r
uint256 totalBTC; // Total BTC amount (in satoshi)\r
}\r
\r
// Withdrawal request structure\r
struct WithdrawalRequest {\r
address user; // User address\r
address ethReceiver; // ETH receiving address\r
address gptReceiver; // GPT receiving address\r
\r
uint40 requestTimestamp; // Request timestamp\r
uint40 processedTimestamp; // Processing timestamp\r
bool processed; // Whether processed\r
\r
uint256 assetBurned; // Amount of Asset burned\r
uint256 ethAmount; // Withdrawable ETH amount (in wei)\r
uint256 gptAmount; // Withdrawable GPT amount (in 10^-18 units)\r
uint256 btcAmount; // Withdrawable BTC amount (in satoshi)\r
\r
string btcReceiver; // BTC receiving address as string\r
}\r
\r
// Asset token interface\r
IERC20 public assetToken;\r
\r
// Current total assets\r
LockedAssets public lockedAssets;\r
\r
// Multi-address support\r
mapping(AssetType => address[]) public ethAssetLockAddresses; // ETH and GPT\r
string[] public btcAssetLockAddresses; // BTC\r
\r
// Unified role management\r
mapping(address => uint8) public userRoles;\r
\r
mapping(uint8 => address[]) private roleHolders;\r
mapping(uint8 => mapping(address => uint256)) private roleIndices;\r
\r
// Track used transaction hashes\r
mapping(bytes32 => bool) public usedTransactionHashes;\r
\r
// Store requests by requestId\r
mapping(uint256 => WithdrawalRequest) public withdrawalRequests;\r
\r
// Ordered list of all request IDs for enumeration\r
uint256[] public allRequestIds;\r
\r
// User's withdrawal request IDs\r
mapping(address => uint256[]) public userWithdrawalRequests;\r
\r
// Events defined with indexed fields for efficient filtering\r
event AssetDeposited(\r
address indexed recorder,\r
AssetType assetType,\r
uint256 amount,\r
string transactionHash,\r
uint256 timestamp\r
);\r
\r
event RequestCreated(\r
address indexed user,\r
uint256 indexed requestId,\r
uint256 assetBurned,\r
uint256 ethAmount, // In wei\r
uint256 gptAmount, // In 10^-18 units\r
uint256 btcAmount, // In satoshi\r
address ethReceiver, // ETH receiving address\r
address gptReceiver, // GPT receiving address\r
string btcReceiver, // BTC receiving address\r
uint256 timestamp\r
);\r
\r
event RequestProcessed(\r
address indexed user,\r
uint256 indexed requestId,\r
string ethTxHash,\r
string gptTxHash,\r
string btcTxHash,\r
uint256 timestamp\r
);\r
\r
event RoleChanged(address indexed user, uint8 role, bool enabled, uint256 timestamp);\r
\r
event TokenRescued(address indexed token, address indexed to, uint256 amount, uint256 timestamp);\r
\r
// New event for threshold updates\r
event MinTxAmountsUpdated(\r
uint256 minEthTxAmount,\r
uint256 minGptTxAmount,\r
uint256 minBtcTxAmount,\r
uint256 timestamp\r
);\r
\r
event TransactionHashUsed(string transactionHash, address user, uint256 timestamp);\r
\r
// New event for asset balance adjustments\r
event AssetBalanceAdjusted(\r
address indexed operator,\r
int256 ethDelta,\r
int256 gptDelta,\r
int256 btcDelta,\r
uint256 newEthBalance,\r
uint256 newGptBalance,\r
uint256 newBtcBalance,\r
string reason,\r
uint256 timestamp\r
);\r
\r
// New events for lock address management\r
event LockAddressAdded(AssetType indexed assetType, address ethAddress, string btcAddress);\r
event LockAddressRemoved(AssetType indexed assetType, address ethAddress, string btcAddress);\r
\r
// Modifiers\r
modifier onlyRole(uint8 role) {\r
if (!hasRole(msg.sender, role)) revert NotAuthorized(msg.sender, role);\r
_;\r
}\r
\r
/**\r
* @dev Constructor\r
* @param _assetToken Asset token address\r
*/\r
constructor(address _assetToken) Ownable(msg.sender) ReentrancyGuard() {\r
if (_assetToken == address(0)) revert InvalidZeroAddress();\r
assetToken = IERC20(_assetToken);\r
}\r
\r
/**\r
* @dev Add an ETH lock address\r
* @param ethAddress Ethereum address for ETH\r
*/\r
function addEthLockAddress(address ethAddress) external onlyOwner {\r
_addEthLockAddress(AssetType.ETH, ethAddress);\r
}\r
\r
/**\r
* @dev Add a GPT lock address\r
* @param ethAddress Ethereum address for GPT\r
*/\r
function addGptLockAddress(address ethAddress) external onlyOwner {\r
_addEthLockAddress(AssetType.GPT, ethAddress);\r
}\r
\r
/**\r
* @dev Add a BTC lock address\r
* @param btcAddress BTC address\r
*/\r
function addBtcLockAddress(string calldata btcAddress) external onlyOwner {\r
_addBtcLockAddress(btcAddress);\r
}\r
\r
/**\r
* @dev Internal helper to add an ETH-type lock address\r
*/\r
function _addEthLockAddress(AssetType assetType, address ethAddress) internal {\r
if (ethAddress == address(0)) revert InvalidZeroAddress();\r
\r
// Check if address already exists\r
address[] storage addresses = ethAssetLockAddresses[assetType];\r
for (uint i = 0; i < addresses.length; i++) {\r
if (addresses[i] == ethAddress) revert AddressAlreadyExists();\r
}\r
\r
// Add address\r
addresses.push(ethAddress);\r
\r
emit LockAddressAdded(assetType, ethAddress, "");\r
}\r
\r
/**\r
* @dev Internal helper to add a BTC lock address\r
*/\r
function _addBtcLockAddress(string calldata btcAddress) internal {\r
if (!isValidBTCAddress(btcAddress)) revert InvalidParameters();\r
\r
// Check if address already exists\r
for (uint i = 0; i < btcAssetLockAddresses.length; i++) {\r
if (keccak256(bytes(btcAssetLockAddresses[i])) == keccak256(bytes(btcAddress)))\r
revert AddressAlreadyExists();\r
}\r
\r
// Add address\r
btcAssetLockAddresses.push(btcAddress);\r
\r
emit LockAddressAdded(AssetType.BTC, address(0), btcAddress);\r
}\r
\r
/**\r
* @dev Remove an ETH lock address\r
* @param ethAddress Ethereum address to remove\r
*/\r
function removeEthLockAddress(address ethAddress) external onlyOwner {\r
_removeEthLockAddress(AssetType.ETH, ethAddress);\r
}\r
\r
/**\r
* @dev Remove a GPT lock address\r
* @param ethAddress Ethereum address to remove\r
*/\r
function removeGptLockAddress(address ethAddress) external onlyOwner {\r
_removeEthLockAddress(AssetType.GPT, ethAddress);\r
}\r
\r
/**\r
* @dev Remove a BTC lock address\r
* @param btcAddress BTC address to remove\r
*/\r
function removeBtcLockAddress(string calldata btcAddress) external onlyOwner {\r
_removeBtcLockAddress(btcAddress);\r
}\r
\r
/**\r
* @dev Internal helper to remove an ETH-type lock address\r
*/\r
function _removeEthLockAddress(AssetType assetType, address ethAddress) internal {\r
address[] storage addresses = ethAssetLockAddresses[assetType];\r
\r
bool found = false;\r
uint256 index;\r
\r
for (uint i = 0; i < addresses.length; i++) {\r
if (addresses[i] == ethAddress) {\r
found = true;\r
index = i;\r
break;\r
}\r
}\r
\r
if (!found) revert AddressNotFound();\r
\r
// Remove address - move last address to the position being deleted, then pop\r
if (index != addresses.length - 1) {\r
addresses[index] = addresses[addresses.length - 1];\r
}\r
addresses.pop();\r
\r
emit LockAddressRemoved(assetType, ethAddress, "");\r
}\r
\r
/**\r
* @dev Internal helper to remove a BTC lock address\r
*/\r
function _removeBtcLockAddress(string calldata btcAddress) internal {\r
bool found = false;\r
uint256 index;\r
\r
for (uint i = 0; i < btcAssetLockAddresses.length; i++) {\r
if (keccak256(bytes(btcAssetLockAddresses[i])) == keccak256(bytes(btcAddress))) {\r
found = true;\r
index = i;\r
break;\r
}\r
}\r
\r
if (!found) revert AddressNotFound();\r
\r
// Remove address\r
if (index != btcAssetLockAddresses.length - 1) {\r
btcAssetLockAddresses[index] = btcAssetLockAddresses[btcAssetLockAddresses.length - 1];\r
}\r
btcAssetLockAddresses.pop();\r
\r
emit LockAddressRemoved(AssetType.BTC, address(0), btcAddress);\r
}\r
\r
/**\r
* @dev Get all lock addresses for all asset types at once\r
*/\r
function getAllLockAddresses() external view returns (\r
address[] memory ethAddresses,\r
address[] memory gptAddresses,\r
string[] memory btcAddresses\r
) {\r
return (\r
ethAssetLockAddresses[AssetType.ETH],\r
ethAssetLockAddresses[AssetType.GPT],\r
btcAssetLockAddresses\r
);\r
}\r
\r
/**\r
* @dev Set minimum transaction amount thresholds\r
* @param _minEthTxAmount Minimum ETH amount that requires a transaction hash (wei)\r
* @param _minGptTxAmount Minimum GPT amount that requires a transaction hash (10^-18)\r
* @param _minBtcTxAmount Minimum BTC amount that requires a transaction hash (satoshi)\r
*/\r
function setMinTxAmounts(uint256 _minEthTxAmount, uint256 _minGptTxAmount, uint256 _minBtcTxAmount) external onlyOwner {\r
minEthTxAmount = _minEthTxAmount;\r
minGptTxAmount = _minGptTxAmount;\r
minBtcTxAmount = _minBtcTxAmount;\r
\r
emit MinTxAmountsUpdated(\r
_minEthTxAmount,\r
_minGptTxAmount,\r
_minBtcTxAmount,\r
block.timestamp\r
);\r
}\r
\r
/**\r
* @dev Adjust locked asset balances by delta amounts\r
* @param ethDelta Delta for ETH amount (positive to add, negative to subtract)\r
* @param gptDelta Delta for GPT amount (positive to add, negative to subtract)\r
* @param btcDelta Delta for BTC amount (positive to add, negative to subtract)\r
* @param reason Reason for the adjustment\r
*/\r
function adjustAssetBalances(int256 ethDelta, int256 gptDelta, int256 btcDelta, string calldata reason) external onlyOwner {\r
// Skip if all deltas are zero\r
if (ethDelta == 0 && gptDelta == 0 && btcDelta == 0) revert InvalidZeroAmount();\r
\r
// Check for sufficient balances when reducing\r
if (ethDelta < 0 && uint256(-ethDelta) > lockedAssets.totalETH) {\r
revert InsufficientBalance("ETH");\r
}\r
\r
if (gptDelta < 0 && uint256(-gptDelta) > lockedAssets.totalGPT) {\r
revert InsufficientBalance("GPT");\r
}\r
\r
if (btcDelta < 0 && uint256(-btcDelta) > lockedAssets.totalBTC) {\r
revert InsufficientBalance("BTC");\r
}\r
\r
// Apply adjustments\r
if (ethDelta != 0) {\r
if (ethDelta > 0) {\r
lockedAssets.totalETH += uint256(ethDelta);\r
} else {\r
lockedAssets.totalETH -= uint256(-ethDelta);\r
}\r
}\r
\r
if (gptDelta != 0) {\r
if (gptDelta > 0) {\r
lockedAssets.totalGPT += uint256(gptDelta);\r
} else {\r
lockedAssets.totalGPT -= uint256(-gptDelta);\r
}\r
}\r
\r
if (btcDelta != 0) {\r
if (btcDelta > 0) {\r
lockedAssets.totalBTC += uint256(btcDelta);\r
} else {\r
lockedAssets.totalBTC -= uint256(-btcDelta);\r
}\r
}\r
\r
// Emit event with deltas and new balances\r
emit AssetBalanceAdjusted(\r
msg.sender,\r
ethDelta,\r
gptDelta,\r
btcDelta,\r
lockedAssets.totalETH,\r
lockedAssets.totalGPT,\r
lockedAssets.totalBTC,\r
reason,\r
block.timestamp\r
);\r
}\r
\r
/**\r
* @dev Check if user has specified role\r
*/\r
function hasRole(address user, uint8 role) public view returns (bool) {\r
return (userRoles[user] & role != 0) || user == owner();\r
}\r
\r
/**\r
* @dev Set user role\r
*/\r
function setRole(address user, uint8 role, bool enabled) external onlyOwner {\r
if (user == address(0)) revert InvalidZeroAddress();\r
\r
// Cache current role state\r
uint8 currentRoles = userRoles[user];\r
bool hasRoleBefore = (currentRoles & role != 0);\r
\r
// Early return to save gas\r
if (hasRoleBefore == enabled) return;\r
\r
if (enabled) {\r
// Update role bitmap\r
userRoles[user] = currentRoles | role;\r
\r
// Add to role holders list if not already there\r
uint256 index = roleIndices[role][user];\r
if (index == 0) {\r
roleHolders[role].push(user);\r
roleIndices[role][user] = roleHolders[role].length;\r
}\r
} else {\r
// Update role bitmap\r
userRoles[user] = currentRoles & ~role;\r
\r
// Remove from role holders list\r
uint256 index = roleIndices[role][user];\r
if (index > 0) {\r
unchecked {\r
index--; // Convert to 0-based index\r
uint256 lastIndex = roleHolders[role].length - 1;\r
\r
// If not the last element, replace with the last element\r
if (index != lastIndex) {\r
address lastAddress = roleHolders[role][lastIndex];\r
roleHolders[role][index] = lastAddress;\r
roleIndices[role][lastAddress] = index + 1;\r
}\r
\r
// Remove last element and clear index\r
roleHolders[role].pop();\r
roleIndices[role][user] = 0;\r
}\r
}\r
}\r
\r
emit RoleChanged(user, role, enabled, block.timestamp);\r
}\r
\r
/**\r
* @dev Get users with specific role\r
*/\r
function getUsersWithRole(uint8 role) public view returns (address[] memory) {\r
return roleHolders[role];\r
}\r
\r
/**\r
* @dev Get all operators (ROLE_RECORDER)\r
*/\r
function getOperators() external view returns (address[] memory) {\r
return getUsersWithRole(ROLE_RECORDER);\r
}\r
\r
/**\r
* @dev Get all processors (ROLE_PROCESSOR)\r
*/\r
function getProcessors() external view returns (address[] memory) {\r
return getUsersWithRole(ROLE_PROCESSOR);\r
}\r
\r
/**\r
* @dev Check if request exists\r
*/\r
function requestExists(uint256 requestId) public view returns (bool) {\r
return withdrawalRequests[requestId].requestTimestamp > 0;\r
}\r
\r
/**\r
* @dev Record asset deposit\r
*/\r
function depositAsset(AssetType assetType, uint256 amount, string calldata transactionHash) external onlyRole(ROLE_RECORDER) {\r
if (amount == 0) revert InvalidZeroAmount();\r
_validateAndRecordTxHash(transactionHash, "");\r
\r
// Use unchecked to update state\r
unchecked {\r
// Update locked assets based on asset type - using concise conditionals\r
if (assetType == AssetType.ETH) {\r
lockedAssets.totalETH += amount;\r
} else if (assetType == AssetType.GPT) {\r
lockedAssets.totalGPT += amount;\r
} else {\r
// Default to BTC type\r
lockedAssets.totalBTC += amount;\r
}\r
}\r
\r
// Emit event\r
emit AssetDeposited(\r
msg.sender,\r
assetType,\r
amount,\r
transactionHash,\r
block.timestamp\r
);\r
}\r
\r
/**\r
* @dev Validate BTC address format\r
* Support for both mainnet and testnet addresses\r
*/\r
function isValidBTCAddress(string memory btcAddr) internal pure returns (bool) {\r
bytes memory b = bytes(btcAddr);\r
\r
// Length check - early return saves gas\r
uint256 len = b.length;\r
if (len < 26 || len > 90) return false;\r
\r
// Single character lookup\r
bytes1 firstChar = b[0];\r
\r
// Check against valid first characters\r
return (\r
firstChar == 0x31 || // '1' - Mainnet P2PKH\r
firstChar == 0x33 || // '3' - Mainnet P2SH\r
firstChar == 0x62 || // 'b' - Mainnet SegWit\r
firstChar == 0x6D || // 'm' - Testnet P2PKH\r
firstChar == 0x6E || // 'n' - Testnet P2PKH\r
firstChar == 0x32 || // '2' - Testnet P2SH\r
firstChar == 0x74 // 't' - Testnet SegWit\r
);\r
}\r
\r
/**\r
* @dev Calculate withdrawal amounts and check thresholds\r
*/\r
function calculateWithdrawalAmounts(uint256 assetAmount) public view returns (uint256 ethToWithdraw, uint256 gptToWithdraw, uint256 btcToWithdraw) {\r
// Cache asset state in single read operations\r
uint256 supply = assetToken.totalSupply();\r
\r
if (supply == 0) return (0, 0, 0);\r
\r
uint256 totalETH = lockedAssets.totalETH;\r
uint256 totalGPT = lockedAssets.totalGPT;\r
uint256 totalBTC = lockedAssets.totalBTC;\r
\r
// Calculate withdrawal amounts with combined operations\r
unchecked {\r
// Direct ratio calculations with simplified conditions\r
ethToWithdraw = totalETH > 0 ? (totalETH * 1e18 / supply * assetAmount) / 1e18 : 0;\r
gptToWithdraw = totalGPT > 0 ? (totalGPT * 1e18 / supply * assetAmount) / 1e18 : 0;\r
btcToWithdraw = totalBTC > 0 ? (totalBTC * 1e18 / supply * assetAmount) / 1e18 : 0;\r
\r
// Truncate GPT to 4 decimal places\r
gptToWithdraw = (gptToWithdraw / 1e14) * 1e14;\r
\r
// Apply minimum thresholds with direct assignment\r
if (ethToWithdraw < minEthTxAmount) ethToWithdraw = 0;\r
if (gptToWithdraw < minGptTxAmount) gptToWithdraw = 0;\r
if (btcToWithdraw < minBtcTxAmount) btcToWithdraw = 0;\r
}\r
\r
return (ethToWithdraw, gptToWithdraw, btcToWithdraw);\r
}\r
\r
/**\r
* @dev Process asset burn and create withdrawal request\r
* @param user User address who burned asset tokens\r
* @param assetAmount Amount of asset tokens burned\r
* @param requestId request ID to identify this withdrawal\r
* @param ethReceiver Address to receive ETH\r
* @param gptReceiver Address to receive GPT tokens\r
* @param btcReceiver BTC address to receive Bitcoin\r
*/\r
function createWithdrawalRequest(\r
address user,\r
uint256 assetAmount,\r
uint256 requestId,\r
address ethReceiver,\r
address gptReceiver,\r
string memory btcReceiver\r
) external nonReentrant {\r
// Security checks\r
if (msg.sender != address(assetToken)) revert UnauthorizedCaller(msg.sender);\r
if (requestExists(requestId)) revert RequestAlreadyProcessed(requestId);\r
\r
// Check if total supply is zero\r
if (assetToken.totalSupply() == 0) revert InvalidParameters();\r
\r
// Calculate withdrawals and update balances\r
(uint256 ethToWithdraw, uint256 gptToWithdraw, uint256 btcToWithdraw) = calculateWithdrawalAmounts(assetAmount);\r
\r
// Check if all withdrawal amounts are zero\r
if (ethToWithdraw == 0 && gptToWithdraw == 0 && btcToWithdraw == 0) {\r
revert ZeroWithdrawalAmount();\r
}\r
\r
if (ethToWithdraw > 0 && ethReceiver == address(0)) {\r
revert InvalidETHReceiverAddress();\r
}\r
\r
if (gptToWithdraw > 0 && gptReceiver == address(0)) {\r
revert InvalidGPTReceiverAddress();\r
}\r
\r
if (btcToWithdraw > 0 && !isValidBTCAddress(btcReceiver)) {\r
revert InvalidBTCReceiverAddress();\r
}\r
\r
uint256 totalETH = lockedAssets.totalETH;\r
uint256 totalGPT = lockedAssets.totalGPT;\r
uint256 totalBTC = lockedAssets.totalBTC;\r
\r
// Combined balance check in one condition\r
if (totalETH < ethToWithdraw || totalGPT < gptToWithdraw || totalBTC < btcToWithdraw) {\r
revert InsufficientAssetBalance(\r
ethToWithdraw, totalETH,\r
gptToWithdraw, totalGPT,\r
btcToWithdraw, totalBTC\r
);\r
}\r
\r
unchecked {\r
lockedAssets.totalETH = totalETH - ethToWithdraw;\r
lockedAssets.totalGPT = totalGPT - gptToWithdraw;\r
lockedAssets.totalBTC = totalBTC - btcToWithdraw;\r
}\r
\r
withdrawalRequests[requestId] = WithdrawalRequest({\r
user: user,\r
ethReceiver: ethToWithdraw > 0 ? ethReceiver : address(0),\r
gptReceiver: gptToWithdraw > 0 ? gptReceiver : address(0),\r
requestTimestamp: uint40(block.timestamp),\r
processedTimestamp: 0,\r
processed: false,\r
assetBurned: assetAmount,\r
ethAmount: ethToWithdraw,\r
gptAmount: gptToWithdraw,\r
btcAmount: btcToWithdraw,\r
btcReceiver: btcToWithdraw > 0 ? btcReceiver : ""\r
});\r
\r
unchecked {\r
allRequestIds.push(requestId);\r
userWithdrawalRequests[user].push(requestId);\r
}\r
\r
emit RequestCreated(\r
user, requestId, assetAmount,\r
ethToWithdraw, gptToWithdraw, btcToWithdraw,\r
ethToWithdraw > 0 ? ethReceiver : address(0),\r
gptToWithdraw > 0 ? gptReceiver : address(0),\r
btcToWithdraw > 0 ? btcReceiver : "",\r
block.timestamp\r
);\r
}\r
\r
/**\r
* @dev Validate and record transaction hash usage\r
*/\r
function _validateAndRecordTxHash(string calldata transactionHash, string memory assetType) internal {\r
if (bytes(transactionHash).length == 0) {\r
if (bytes(assetType).length > 0) {\r
revert MissingTransactionHash(assetType);\r
} else {\r
revert EmptyTransactionHash();\r
}\r
}\r
\r
bytes32 txHashDigest = keccak256(abi.encodePacked(transactionHash));\r
\r
if (usedTransactionHashes[txHashDigest]) {\r
revert TransactionHashAlreadyUsed(transactionHash);\r
}\r
\r
usedTransactionHashes[txHashDigest] = true;\r
\r
emit TransactionHashUsed(transactionHash, msg.sender, block.timestamp);\r
}\r
\r
/**\r
* @dev Mark withdrawal request as processed\r
*/\r
function processWithdrawalRequest(\r
uint256 requestId,\r
string calldata ethTxHash,\r
string calldata gptTxHash,\r
string calldata btcTxHash\r
) external onlyRole(ROLE_PROCESSOR) {\r
WithdrawalRequest storage request = withdrawalRequests[requestId];\r
\r
if (request.user == address(0)) revert InvalidRequestId(requestId);\r
if (request.processed) revert RequestAlreadyProcessed(requestId);\r
\r
if (request.ethAmount > 0) {\r
_validateAndRecordTxHash(ethTxHash, "ETH");\r
}\r
\r
if (request.gptAmount > 0) {\r
_validateAndRecordTxHash(gptTxHash, "GPT");\r
}\r
\r
if (request.btcAmount > 0) {\r
_validateAndRecordTxHash(btcTxHash, "BTC");\r
}\r
\r
request.processed = true;\r
request.processedTimestamp = uint40(block.timestamp);\r
\r
emit RequestProcessed(\r
request.user,\r
requestId,\r
ethTxHash,\r
gptTxHash,\r
btcTxHash,\r
block.timestamp\r
);\r
}\r
\r
/**\r
* @dev Get a range of request IDs\r
*/\r
function getRequestIds(uint256 start, uint256 limit, bool processedOnly) external view returns (uint256[] memory) {\r
uint256 totalRequests = allRequestIds.length;\r
uint256[] memory result;\r
\r
if (limit > MAX_REQUEST_LIMIT) {\r
limit = MAX_REQUEST_LIMIT;\r
}\r
\r
if (start >= totalRequests || limit == 0) {\r
return new uint256[](0);\r
}\r
\r
uint256 availableItems;\r
unchecked {\r
availableItems = totalRequests - start;\r
}\r
uint256 maxItems = availableItems < limit ? availableItems : limit;\r
\r
if (!processedOnly) {\r
result = new uint256[](maxItems);\r
\r
unchecked {\r
for (uint256 i = 0; i < maxItems; i++) {\r
result[i] = allRequestIds[start + i];\r
}\r
}\r
return result;\r
}\r
\r
uint256 validCount = 0;\r
uint256 endIndex = start + maxItems < totalRequests ? start + maxItems : totalRequests;\r
\r
unchecked {\r
for (uint256 i = start; i < endIndex; i++) {\r
if (withdrawalRequests[allRequestIds[i]].processed) {\r
validCount++;\r
}\r
}\r
}\r
\r
result = new uint256[](validCount);\r
\r
if (validCount > 0) {\r
uint256 resultIndex = 0;\r
\r
unchecked {\r
for (uint256 i = start; i < endIndex && resultIndex < validCount; i++) {\r
uint256 reqId = allRequestIds[i];\r
if (withdrawalRequests[reqId].processed) {\r
result[resultIndex++] = reqId;\r
}\r
}\r
}\r
}\r
\r
return result;\r
}\r
\r
/**\r
* @dev Get total number of requests\r
*/\r
function getRequestCount() external view returns (uint256) {\r
return allRequestIds.length;\r
}\r
\r
/**\r
* @dev Get user's withdrawal request list\r
*/\r
function getUserRequests(address user, uint256 start, uint256 limit) external view returns (uint256[] memory) {\r
if (limit > MAX_REQUEST_LIMIT) {\r
limit = MAX_REQUEST_LIMIT;\r
}\r
\r
uint256[] memory userRequests = userWithdrawalRequests[user];\r
uint256 totalUserRequests = userRequests.length;\r
\r
if (start >= totalUserRequests || limit == 0) {\r
return new uint256[](0);\r
}\r
\r
uint256 availableItems;\r
unchecked {\r
availableItems = totalUserRequests - start;\r
}\r
uint256 maxItems = availableItems < limit ? availableItems : limit;\r
\r
uint256[] memory result = new uint256[](maxItems);\r
\r
unchecked {\r
for (uint256 i = 0; i < maxItems; i++) {\r
result[i] = userRequests[start + i];\r
}\r
}\r
\r
return result;\r
}\r
\r
/**\r
* @dev Get user request count\r
*/\r
function getUserRequestCount(address user) external view returns (uint256) {\r
return userWithdrawalRequests[user].length;\r
}\r
\r
/**\r
* @dev Get request details\r
*/\r
function getRequest(uint256 requestId) external view returns (\r
address user,\r
uint256 assetBurned,\r
uint256 ethAmount,\r
uint256 gptAmount,\r
uint256 btcAmount,\r
uint256 requestTimestamp,\r
bool processed,\r
uint256 processedTimestamp,\r
address ethReceiver,\r
address gptReceiver,\r
string memory btcReceiver\r
) {\r
WithdrawalRequest memory request = withdrawalRequests[requestId];\r
if (request.user == address(0)) revert InvalidRequestId(requestId);\r
\r
return (\r
request.user,\r
request.assetBurned,\r
request.ethAmount,\r
request.gptAmount,\r
request.btcAmount,\r
request.requestTimestamp,\r
request.processed,\r
request.processedTimestamp,\r
request.ethReceiver,\r
request.gptReceiver,\r
request.btcReceiver\r
);\r
}\r
\r
/**\r
* @dev Get current asset ratio\r
*/\r
function getAssetRatio() external view returns (uint256 ethPerAsset, uint256 gptPerAsset, uint256 btcPerAsset) {\r
uint256 supply = assetToken.totalSupply();\r
\r
if (supply == 0) return (0, 0, 0);\r
\r
uint256 totalETH = lockedAssets.totalETH;\r
uint256 totalGPT = lockedAssets.totalGPT;\r
uint256 totalBTC = lockedAssets.totalBTC;\r
\r
unchecked {\r
ethPerAsset = totalETH > 0 ? (totalETH * 1e18) / supply : 0;\r
gptPerAsset = totalGPT > 0 ? (totalGPT * 1e18) / supply : 0;\r
btcPerAsset = totalBTC > 0 ? (totalBTC * 1e18) / supply : 0;\r
}\r
}\r
\r
/**\r
* @dev Get total locked assets\r
*/\r
function getLockedAssets() external view returns (uint256 totalETH, uint256 totalGPT, uint256 totalBTC) {\r
return (\r
lockedAssets.totalETH,\r
lockedAssets.totalGPT,\r
lockedAssets.totalBTC\r
);\r
}\r
\r
/**\r
* @dev Rescue tokens\r
*/\r
function rescueToken(IERC20 token, uint256 amount) external onlyOwner {\r
if (address(token) == address(0)) revert InvalidZeroAddress();\r
if (amount == 0) revert InvalidZeroAmount();\r
\r
address ownerAddr = owner();\r
token.transfer(ownerAddr, amount);\r
\r
emit TokenRescued(address(token), ownerAddr, amount, block.timestamp);\r
}\r
}"
},
"@openzeppelin/contracts/token/ERC20/IERC20.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC-20 standard as defined in the ERC.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
"
},
"@openzeppelin/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;
}
}
"
},
"@openzeppelin/contracts/access/Ownable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
"
},
"@openzeppelin/contracts/utils/Context.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}
"
}
},
"settings": {
"optimizer": {
"enabled": true,
"runs": 200
},
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"devdoc",
"userdoc",
"metadata",
"abi"
]
}
},
"remappings": []
}
}}
Submitted on: 2025-09-22 12:53:05
Comments
Log in to comment.
No comments yet.