From 4af638741992740c4bd9b25dfeeb7fa84c677e7c Mon Sep 17 00:00:00 2001 From: xiaoch05 Date: Tue, 19 Dec 2023 19:32:46 +0800 Subject: [PATCH] guard files --- .../mapping-token/v3/GuardRegistryV3.sol | 297 ++++++++++++++++++ .../contracts/mapping-token/v3/GuardV3.sol | 158 ++++++++++ 2 files changed, 455 insertions(+) create mode 100644 helix-contract/contracts/mapping-token/v3/GuardRegistryV3.sol create mode 100644 helix-contract/contracts/mapping-token/v3/GuardV3.sol diff --git a/helix-contract/contracts/mapping-token/v3/GuardRegistryV3.sol b/helix-contract/contracts/mapping-token/v3/GuardRegistryV3.sol new file mode 100644 index 0000000..83702dd --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/GuardRegistryV3.sol @@ -0,0 +1,297 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.10; +pragma experimental ABIEncoderV2; + +import "@zeppelin-solidity/contracts/utils/cryptography/ECDSA.sol"; + +/** + * @title Manages a set of guards and a threshold to double-check BEEFY commitment + * @dev Stores the guards and a threshold + * @author echo + */ +contract GuardRegistryV3 { + event AddedGuard(address guard); + event RemovedGuard(address guard); + event ChangedThreshold(uint256 threshold); + + // keccak256( + // "EIP712Domain(uint256 chainId,address verifyingContract)" + // ); + bytes32 internal constant DOMAIN_SEPARATOR_TYPEHASH = 0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218; + + address internal constant SENTINEL_GUARDS = address(0x1); + + /** + * @dev Nonce to prevent replay of update operations + */ + uint256 public nonce; + /** + * @dev Store all guards in the linked list + */ + mapping(address => address) internal guards; + /** + * @dev Count of all guards + */ + uint256 internal guardCount; + /** + * @dev Number of required confirmations for update operations + */ + uint256 internal threshold; + + /** + * @dev Sets initial storage of contract. + * @param _guards List of Safe guards. + * @param _threshold Number of required confirmations for check commitment or change guards. + */ + function initialize(address[] memory _guards, uint256 _threshold) internal { + // Threshold can only be 0 at initialization. + // Check ensures that setup function can only be called once. + require(threshold == 0, "Guard: Guards have already been setup"); + // Validate that threshold is smaller than number of added guards. + require(_threshold <= _guards.length, "Guard: Threshold cannot exceed guard count"); + // There has to be at least one Safe guard. + require(_threshold >= 1, "Guard: Threshold needs to be greater than 0"); + // Initializing Safe guards. + address currentGuard = SENTINEL_GUARDS; + for (uint256 i = 0; i < _guards.length; i++) { + // Guard address cannot be null. + address guard = _guards[i]; + require(guard != address(0) && guard != SENTINEL_GUARDS && guard != address(this) && currentGuard != guard, "Guard: Invalid guard address provided"); + // No duplicate guards allowed. + require(guards[guard] == address(0), "Guard: Address is already an guard"); + guards[currentGuard] = guard; + currentGuard = guard; + emit AddedGuard(guard); + } + guards[currentGuard] = SENTINEL_GUARDS; + guardCount = _guards.length; + threshold = _threshold; + } + + /** + * @dev Allows to add a new guard to the registry and update the threshold at the same time. + * This can only be done via multi-sig. + * @notice Adds the guard `guard` to the registry and updates the threshold to `_threshold`. + * @param guard New guard address. + * @param _threshold New threshold. + * @param signatures The signatures of the guards which to add new guard and update the `threshold` . + */ + function addGuardWithThreshold( + address guard, + uint256 _threshold, + bytes[] memory signatures + ) public { + // Guard address cannot be null, the sentinel or the registry itself. + require(guard != address(0) && guard != SENTINEL_GUARDS && guard != address(this), "Guard: Invalid guard address provided"); + // No duplicate guards allowed. + require(guards[guard] == address(0), "Guard: Address is already an guard"); + verifyGuardSignatures(msg.sig, abi.encode(guard, _threshold), signatures); + guards[guard] = guards[SENTINEL_GUARDS]; + guards[SENTINEL_GUARDS] = guard; + guardCount++; + emit AddedGuard(guard); + // Change threshold if threshold was changed. + if (threshold != _threshold) _changeThreshold(_threshold); + } + + /** + * @dev Allows to remove an guard from the registry and update the threshold at the same time. + * This can only be done via multi-sig. + * @notice Removes the guard `guard` from the registry and updates the threshold to `_threshold`. + * @param prevGuard Guard that pointed to the guard to be removed in the linked list + * @param guard Guard address to be removed. + * @param _threshold New threshold. + * @param signatures The signatures of the guards which to remove a guard and update the `threshold` . + */ + function removeGuard( + address prevGuard, + address guard, + uint256 _threshold, + bytes[] memory signatures + ) public { + // Only allow to remove an guard, if threshold can still be reached. + require(guardCount - 1 >= _threshold, "Guard: Threshold cannot exceed guard count"); + // Validate guard address and check that it corresponds to guard index. + require(guard != address(0) && guard != SENTINEL_GUARDS, "Guard: Invalid guard address provided"); + require(guards[prevGuard] == guard, "Guard: Invalid prevGuard, guard pair provided"); + verifyGuardSignatures(msg.sig, abi.encode(prevGuard, guard, _threshold), signatures); + guards[prevGuard] = guards[guard]; + guards[guard] = address(0); + guardCount--; + emit RemovedGuard(guard); + // Change threshold if threshold was changed. + if (threshold != _threshold) _changeThreshold(_threshold); + } + + /** + * @dev Allows to swap/replace a guard from the registry with another address. + * This can only be done via multi-sig. + * @notice Replaces the guard `oldGuard` in the registry with `newGuard`. + * @param prevGuard guard that pointed to the guard to be replaced in the linked list + * @param oldGuard guard address to be replaced. + * @param newGuard New guard address. + * @param signatures The signatures of the guards which to swap/replace a guard and update the `threshold` . + */ + function swapGuard( + address prevGuard, + address oldGuard, + address newGuard, + bytes[] memory signatures + ) public { + // Guard address cannot be null, the sentinel or the registry itself. + require(newGuard != address(0) && newGuard != SENTINEL_GUARDS && newGuard != address(this), "Guard: Invalid guard address provided"); + // No duplicate guards allowed. + require(guards[newGuard] == address(0), "Guard: Address is already an guard"); + // Validate oldGuard address and check that it corresponds to guard index. + require(oldGuard != address(0) && oldGuard != SENTINEL_GUARDS, "Guard: Invalid guard address provided"); + require(guards[prevGuard] == oldGuard, "Guard: Invalid prevGuard, guard pair provided"); + verifyGuardSignatures(msg.sig, abi.encode(prevGuard, oldGuard, newGuard), signatures); + guards[newGuard] = guards[oldGuard]; + guards[prevGuard] = newGuard; + guards[oldGuard] = address(0); + emit RemovedGuard(oldGuard); + emit AddedGuard(newGuard); + } + + /** + * @dev Allows to update the number of required confirmations by guards. + * This can only be done via multi-sig. + * @notice Changes the threshold of the registry to `_threshold`. + * @param _threshold New threshold. + * @param signatures The signatures of the guards which to update the `threshold` . + */ + function changeThreshold(uint256 _threshold, bytes[] memory signatures) public { + verifyGuardSignatures(msg.sig, abi.encode(_threshold), signatures); + _changeThreshold(_threshold); + } + + function _changeThreshold(uint256 _threshold) internal { + // Validate that threshold is smaller than number of owners. + require(_threshold <= guardCount, "Guard: Threshold cannot exceed guard count"); + // There has to be at least one guard. + require(_threshold >= 1, "Guard: Threshold needs to be greater than 0"); + threshold = _threshold; + emit ChangedThreshold(threshold); + } + + function getThreshold() public view returns (uint256) { + return threshold; + } + + function isGuard(address guard) public view returns (bool) { + return guard != SENTINEL_GUARDS && guards[guard] != address(0); + } + + /** + * @dev Returns array of guards. + * @return Array of guards. + */ + function getGuards() public view returns (address[] memory) { + address[] memory array = new address[](guardCount); + + // populate return array + uint256 index = 0; + address currentGuard = guards[SENTINEL_GUARDS]; + while (currentGuard != SENTINEL_GUARDS) { + array[index] = currentGuard; + currentGuard = guards[currentGuard]; + index++; + } + return array; + } + + function verifyGuardSignatures( + bytes4 methodID, + bytes memory params, + bytes[] memory signatures + ) internal { + bytes32 structHash = + keccak256( + abi.encode( + methodID, + params, + nonce + ) + ); + checkGuardSignatures(structHash, signatures); + nonce++; + } + + function verifyGuardSignaturesWithoutNonce( + bytes4 methodID, + bytes memory params, + bytes[] memory signatures + ) view internal { + bytes32 structHash = + keccak256( + abi.encode( + methodID, + params + ) + ); + checkGuardSignatures(structHash, signatures); + } + + /** + * @dev Checks whether the signature provided is valid for the provided data, hash. Will revert otherwise. + * @param structHash The struct Hash of the data (could be either a message/commitment hash). + * @param signatures Signature data that should be verified. only ECDSA signature. + * Signers need to be sorted in ascending order + */ + function checkGuardSignatures( + bytes32 structHash, + bytes[] memory signatures + ) public view { + // Load threshold to avoid multiple storage loads + uint256 _threshold = threshold; + // Check that a threshold is set + require(_threshold > 0, "Guard: Threshold needs to be defined"); + bytes32 dataHash = encodeDataHash(structHash); + checkNSignatures(dataHash, signatures, _threshold); + } + + /** + * @dev Checks whether the signature provided is valid for the provided data, hash. Will revert otherwise. + * @param dataHash Hash of the data (could be either a message hash or transaction hash). + * @param signatures Signature data that should be verified. only ECDSA signature. + * Signers need to be sorted in ascending order + * @param requiredSignatures Amount of required valid signatures. + */ + function checkNSignatures( + bytes32 dataHash, + bytes[] memory signatures, + uint256 requiredSignatures + ) public view { + // Check that the provided signature data is not too short + require(signatures.length >= requiredSignatures, "GS020"); + // There cannot be an owner with address 0. + address lastGuard = address(0); + address currentGuard; + for (uint256 i = 0; i < requiredSignatures; i++) { + currentGuard = ECDSA.recover(dataHash, signatures[i]); + require(currentGuard > lastGuard && guards[currentGuard] != address(0) && currentGuard != SENTINEL_GUARDS, "Guard: Invalid guard provided"); + lastGuard = currentGuard; + } + } + + /** + * @dev Returns the chain id used by this contract. + */ + function getChainId() public view returns (uint256) { + uint256 id; + // solhint-disable-next-line no-inline-assembly + assembly { + id := chainid() + } + return id; + } + + function domainSeparator() public view returns (bytes32) { + return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, getChainId(), address(this))); + } + + function encodeDataHash(bytes32 structHash) public view returns (bytes32) { + return keccak256(abi.encodePacked(hex"1901", domainSeparator(), structHash)); + } +} diff --git a/helix-contract/contracts/mapping-token/v3/GuardV3.sol b/helix-contract/contracts/mapping-token/v3/GuardV3.sol new file mode 100644 index 0000000..d0441f3 --- /dev/null +++ b/helix-contract/contracts/mapping-token/v3/GuardV3.sol @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity >=0.8.17; + +import "@zeppelin-solidity/contracts/security/Pausable.sol"; +import "@zeppelin-solidity/contracts/token/ERC20/IERC20.sol"; +import "@zeppelin-solidity/contracts/utils/math/SafeMath.sol"; +import "./GuardRegistryV3.sol"; +import "../interfaces/IWToken.sol"; + +contract GuardV3 is GuardRegistryV3, Pausable { + using SafeMath for uint256; + + mapping(uint256 => bytes32) deposits; + + uint256 public maxUnclaimableTime; + mapping(address => bool) depositors; + address public operator; + + event TokenDeposit(address sender, uint256 id, uint256 timestamp, address token, address recipient, uint256 amount); + event TokenClaimed(uint256 id); + + constructor(address[] memory _guards, uint256 _threshold, uint256 _maxUnclaimableTime) { + maxUnclaimableTime = _maxUnclaimableTime; + operator = msg.sender; + initialize(_guards, _threshold); + } + + modifier onlyDepositor() { + require(depositors[msg.sender] == true, "Guard: Invalid depositor"); + _; + } + + modifier onlyOperator() { + require(msg.sender == operator, "Guard: Invalid operator"); + _; + } + + function unpause() external onlyOperator { + _unpause(); + } + + function pause() external onlyOperator { + _pause(); + } + + function setOperator(address newOperator, bytes[] memory signatures) external { + verifyGuardSignatures(msg.sig, abi.encode(newOperator), signatures); + operator = newOperator; + } + + function setDepositor(address depositor, bool enable) external onlyOperator { + depositors[depositor] = enable; + } + + function setMaxUnclaimableTime(uint256 _maxUnclaimableTime) external onlyOperator { + maxUnclaimableTime = _maxUnclaimableTime; + } + + /** + * @dev deposit token to guard, waiting to claim, only allowed depositor + * @param id the id of the operation, should be siged later by guards + * @param token the erc20 token address + * @param recipient the recipient of the token + * @param amount the amount of the token + */ + function deposit( + uint256 id, + address token, + address recipient, + uint256 amount + ) public onlyDepositor whenNotPaused { + deposits[id] = hash(abi.encodePacked(msg.sender, block.timestamp, token, recipient, amount)); + emit TokenDeposit(msg.sender, id, block.timestamp, token, recipient, amount); + } + + function claimById( + address from, + uint256 id, + uint256 timestamp, + address token, + address recipient, + uint256 amount, + bool isNative + ) internal { + require(hash(abi.encodePacked(from, timestamp, token, recipient, amount)) == deposits[id], "Guard: Invalid id to claim"); + require(amount > 0, "Guard: Invalid amount to claim"); + if (isNative) { + require(IERC20(token).transferFrom(from, address(this), amount), "Guard: claim native token failed"); + uint256 balanceBefore = address(this).balance; + IWToken(token).withdraw(amount); + require(address(this).balance == balanceBefore.add(amount), "Guard: token is not wrapped by native token"); + payable(recipient).transfer(amount); + } else { + require(IERC20(token).transferFrom(from, recipient, amount), "Guard: claim token failed"); + } + delete deposits[id]; + emit TokenClaimed(id); + } + + /** + * @dev claim the tokens in the contract saved by deposit, this acquire signatures from guards + * @param id the id to be claimed + * @param signatures the signatures of the guards which to claim tokens. + */ + function claim( + address from, + uint256 id, + uint256 timestamp, + address token, + address recipient, + uint256 amount, + bytes[] memory signatures + ) public { + verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(from, id, timestamp, token, recipient, amount), signatures); + claimById(from, id, timestamp, token, recipient, amount, false); + } + + /** + * @dev claimNative the tokens in the contract saved by deposit, this acquire signatures from guards + * @param id the id to be claimed + * @param signatures the signatures of the guards which to claim tokens. + */ + function claimNative( + address from, + uint256 id, + uint256 timestamp, + address token, + address recipient, + uint256 amount, + bytes[] memory signatures + ) public { + verifyGuardSignaturesWithoutNonce(msg.sig, abi.encode(from, id, timestamp, token, recipient, amount), signatures); + claimById(from, id, timestamp, token, recipient, amount, true); + } + + /** + * @dev claim the tokens without signatures, this only allowed when timeout + * @param id the id to be claimed + */ + function claimByTimeout( + address from, + uint256 id, + uint256 timestamp, + address token, + address recipient, + uint256 amount, + bool isNative + ) public whenNotPaused { + require(timestamp < block.timestamp && block.timestamp - timestamp > maxUnclaimableTime, "Guard: claim at invalid time"); + claimById(from, id, timestamp, token, recipient, amount, isNative); + } + + function hash(bytes memory value) public pure returns (bytes32) { + return sha256(value); + } +} +