diff --git a/proxy/DEPLOYED b/proxy/DEPLOYED index a939f53a1..10e7a5846 100644 --- a/proxy/DEPLOYED +++ b/proxy/DEPLOYED @@ -1 +1 @@ -1.3.2-stable.1 \ No newline at end of file +1.3.4-stable.0 \ No newline at end of file diff --git a/proxy/contracts/mainnet/DepositBoxes/DepositBoxERC20.sol b/proxy/contracts/mainnet/DepositBoxes/DepositBoxERC20.sol index 01a598a85..4f25abadf 100644 --- a/proxy/contracts/mainnet/DepositBoxes/DepositBoxERC20.sol +++ b/proxy/contracts/mainnet/DepositBoxes/DepositBoxERC20.sol @@ -25,7 +25,8 @@ import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/structs/DoubleEndedQueueUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; import "@skalenetwork/ima-interfaces/mainnet/DepositBoxes/IDepositBoxERC20.sol"; import "../../Messages.sol"; @@ -47,6 +48,8 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { using AddressUpgradeable for address; using DoubleEndedQueueUpgradeable for DoubleEndedQueueUpgradeable.Bytes32Deque; using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet; + using SafeERC20Upgradeable for IERC20MetadataUpgradeable; + using SafeERC20Upgradeable for IERC20Upgradeable; enum DelayedTransferStatus { DELAYED, @@ -68,10 +71,9 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { mapping(address => uint256) bigTransferThreshold; EnumerableSetUpgradeable.AddressSet trustedReceivers; uint256 transferDelay; - uint256 arbitrageDuration; + uint256 arbitrageDuration; } - address private constant _USDT_ADDRESS = 0xdAC17F958D2ee523a2206206994597C13D831ec7; uint256 private constant _QUEUE_PROCESSING_LIMIT = 10; bytes32 public constant ARBITER_ROLE = keccak256("ARBITER_ROLE"); @@ -85,19 +87,19 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { // exits delay configuration // schainHash => delay config - mapping(bytes32 => DelayConfig) private _delayConfig; + mapping(bytes32 => DelayConfig) private _delayConfig; uint256 public delayedTransfersSize; // delayed transfer id => delayed transfer mapping(uint256 => DelayedTransfer) public delayedTransfers; // receiver address => delayed transfers ids queue - mapping(address => DoubleEndedQueueUpgradeable.Bytes32Deque) public delayedTransfersByReceiver; + mapping(address => DoubleEndedQueueUpgradeable.Bytes32Deque) public delayedTransfersByReceiver; /** * @dev Emitted when token is mapped in DepositBoxERC20. */ event ERC20TokenAdded(string schainName, address indexed contractOnMainnet); - + /** * @dev Emitted when token is received by DepositBox and is ready to be cloned * or transferred on SKALE chain. @@ -108,11 +110,44 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { event Escalated(uint256 id); + /** + * @dev Emitted when token transfer is skipped due to internal token error + */ + event TransferSkipped(uint256 id); + + /** + * @dev Emitted when big transfer threshold is changed + */ + event BigTransferThresholdIsChanged( + bytes32 indexed schainHash, + address indexed token, + uint256 oldValue, + uint256 newValue + ); + + /** + * @dev Emitted when big transfer delay is changed + */ + event BigTransferDelayIsChanged( + bytes32 indexed schainHash, + uint256 oldValue, + uint256 newValue + ); + + /** + * @dev Emitted when arbitrage duration is changed + */ + event ArbitrageDurationIsChanged( + bytes32 indexed schainHash, + uint256 oldValue, + uint256 newValue + ); + /** * @dev Allows `msg.sender` to send ERC20 token from mainnet to schain - * + * * Requirements: - * + * * - Schain name must not be `Mainnet`. * - Receiver account on schain cannot be null. * - Schain that receives tokens should not be killed. @@ -133,7 +168,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { address contractReceiver = schainLinks[schainHash]; require(contractReceiver != address(0), "Unconnected chain"); require( - ERC20Upgradeable(erc20OnMainnet).allowance(msg.sender, address(this)) >= amount, + IERC20MetadataUpgradeable(erc20OnMainnet).allowance(msg.sender, address(this)) >= amount, "DepositBox was not approved for ERC20 token" ); bytes memory data = _receiveERC20( @@ -143,21 +178,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { amount ); _saveTransferredAmount(schainHash, erc20OnMainnet, amount); - if (erc20OnMainnet == _USDT_ADDRESS) { - // solhint-disable-next-line no-empty-blocks - try IERC20TransferVoid(erc20OnMainnet).transferFrom(msg.sender, address(this), amount) {} catch { - revert("Transfer was failed"); - } - } else { - require( - ERC20Upgradeable(erc20OnMainnet).transferFrom( - msg.sender, - address(this), - amount - ), - "Transfer was failed" - ); - } + IERC20MetadataUpgradeable(erc20OnMainnet).safeTransferFrom(msg.sender, address(this), amount); messageProxy.postOutgoingMessage( schainHash, contractReceiver, @@ -167,9 +188,9 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { /** * @dev Allows MessageProxyForMainnet contract to execute transferring ERC20 token from schain to mainnet. - * + * * Requirements: - * + * * - Schain from which the tokens came should not be killed. * - Sender contract should be defined and schain name cannot be `Mainnet`. * - Amount of tokens on DepositBoxERC20 should be equal or more than transferred amount. @@ -187,7 +208,10 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { { Messages.TransferErc20Message memory message = Messages.decodeTransferErc20Message(data); require(message.token.isContract(), "Given address is not a contract"); - require(ERC20Upgradeable(message.token).balanceOf(address(this)) >= message.amount, "Not enough money"); + require( + IERC20MetadataUpgradeable(message.token).balanceOf(address(this)) >= message.amount, + "Not enough money" + ); _removeTransferredAmount(schainHash, message.token, message.amount); uint256 delay = _delayConfig[schainHash].transferDelay; @@ -196,19 +220,19 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { && _delayConfig[schainHash].bigTransferThreshold[message.token] <= message.amount && !isReceiverTrusted(schainHash, message.receiver) ) { - _createDelayedTransfer(schainHash, message, delay); + _createDelayedTransfer(schainHash, message, delay); } else { - _transfer(message.token, message.receiver, message.amount); + IERC20MetadataUpgradeable(message.token).safeTransfer(message.receiver, message.amount); } } /** * @dev Allows Schain owner to add an ERC20 token to DepositBoxERC20. - * + * * Emits an {ERC20TokenAdded} event. - * + * * Requirements: - * + * * - Schain should not be killed. * - Only owner of the schain able to run function. */ @@ -223,11 +247,11 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { /** * @dev Allows Schain owner to return each user their tokens. - * The Schain owner decides which tokens to send to which address, + * The Schain owner decides which tokens to send to which address, * since the contract on mainnet does not store information about which tokens belong to whom. * * Requirements: - * + * * - Amount of tokens on schain should be equal or more than transferred amount. * - msg.sender should be an owner of schain * - IMA transfers Mainnet <-> schain should be killed @@ -241,10 +265,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { bytes32 schainHash = _schainHash(schainName); require(transferredAmount[schainHash][erc20OnMainnet] >= amount, "Incorrect amount"); _removeTransferredAmount(schainHash, erc20OnMainnet, amount); - require( - ERC20Upgradeable(erc20OnMainnet).transfer(receiver, amount), - "Transfer was failed" - ); + IERC20MetadataUpgradeable(erc20OnMainnet).safeTransfer(receiver, amount); } /** @@ -254,7 +275,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { * and can be canceled by a voting * * Requirements: - * + * * - msg.sender should be an owner of schain */ function setBigTransferValue( @@ -267,7 +288,13 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { onlySchainOwner(schainName) { bytes32 schainHash = _schainHash(schainName); - _delayConfig[schainHash].bigTransferThreshold[token] = value; + emit BigTransferThresholdIsChanged( + schainHash, + token, + _delayConfig[schainHash].bigTransferThreshold[token], + value + ); + _delayConfig[schainHash].bigTransferThreshold[token] = value; } /** @@ -277,7 +304,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { * and can be canceled by a voting * * Requirements: - * + * * - msg.sender should be an owner of schain */ function setBigTransferDelay( @@ -291,7 +318,8 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { bytes32 schainHash = _schainHash(schainName); // need to restrict big delays to avoid overflow require(delayInSeconds < 1e8, "Delay is too big"); // no more then ~ 3 years - _delayConfig[schainHash].transferDelay = delayInSeconds; + emit BigTransferDelayIsChanged(schainHash, _delayConfig[schainHash].transferDelay, delayInSeconds); + _delayConfig[schainHash].transferDelay = delayInSeconds; } /** @@ -299,7 +327,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { * After escalation the transfer is locked for provided period of time. * * Requirements: - * + * * - msg.sender should be an owner of schain */ function setArbitrageDuration( @@ -313,13 +341,14 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { bytes32 schainHash = _schainHash(schainName); // need to restrict big delays to avoid overflow require(delayInSeconds < 1e8, "Delay is too big"); // no more then ~ 3 years - _delayConfig[schainHash].arbitrageDuration = delayInSeconds; + emit ArbitrageDurationIsChanged(schainHash, _delayConfig[schainHash].arbitrageDuration, delayInSeconds); + _delayConfig[schainHash].arbitrageDuration = delayInSeconds; } /** * @dev Add the address to a whitelist of addresses that can do big transfers without delaying * Requirements: - * + * * - msg.sender should be an owner of schain * - the address must not be in the whitelist */ @@ -334,13 +363,13 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { require( _delayConfig[_schainHash(schainName)].trustedReceivers.add(receiver), "Receiver already is trusted" - ); + ); } /** * @dev Remove the address from a whitelist of addresses that can do big transfers without delaying * Requirements: - * + * * - msg.sender should be an owner of schain * - the address must be in the whitelist */ @@ -367,7 +396,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { * @dev Initialize arbitrage of a suspicious big transfer * * Requirements: - * + * * - msg.sender should be an owner of schain or have ARBITER_ROLE role * - transfer must be delayed and arbitrage must not be started */ @@ -390,7 +419,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { * @dev Approve a big transfer and immidiately transfer tokens during arbitrage * * Requirements: - * + * * - msg.sender should be an owner of schain * - arbitrage of the transfer must be started */ @@ -405,14 +434,14 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { require(transfer.status == DelayedTransferStatus.ARBITRAGE, "Arbitrage has to be active"); transfer.status = DelayedTransferStatus.COMPLETED; delete transfer.untilTimestamp; - _transfer(transfer.token, transfer.receiver, transfer.amount); + IERC20MetadataUpgradeable(transfer.token).safeTransfer(transfer.receiver, transfer.amount); } /** * @dev Reject a big transfer and transfer tokens to SKALE chain owner during arbitrage * * Requirements: - * + * * - msg.sender should be an owner of schain * - arbitrage of the transfer must be started */ @@ -428,7 +457,12 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { transfer.status = DelayedTransferStatus.COMPLETED; delete transfer.untilTimestamp; // msg.sender is schain owner - _transfer(transfer.token, msg.sender, transfer.amount); + IERC20MetadataUpgradeable(transfer.token).safeTransfer(msg.sender, transfer.amount); + } + + function doTransfer(address token, address receiver, uint256 amount) external override { + require(msg.sender == address(this), "Internal use only"); + IERC20Upgradeable(token).safeTransfer(receiver, amount); } /** @@ -454,7 +488,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { } /** - * @dev Should return true if token was added by Schain owner or + * @dev Should return true if token was added by Schain owner or * added automatically after sending to schain if whitelist was turned off. */ function getSchainToERC20( @@ -470,7 +504,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { } /** - * @dev Should return length of a set of all mapped tokens which were added by Schain owner + * @dev Should return length of a set of all mapped tokens which were added by Schain owner * or added automatically after sending to schain if whitelist was turned off. */ function getSchainToAllERC20Length(string calldata schainName) external view override returns (uint256) { @@ -478,7 +512,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { } /** - * @dev Should return an array of range of tokens were added by Schain owner + * @dev Should return an array of range of tokens were added by Schain owner * or added automatically after sending to schain if whitelist was turned off. */ function getSchainToAllERC20( @@ -593,7 +627,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { bool retrieved = false; for (uint256 i = 0; i < transfersAmount; ++i) { uint256 transferId = uint256(delayedTransfersByReceiver[receiver].at(currentIndex)); - DelayedTransfer memory transfer = delayedTransfers[transferId]; + DelayedTransfer memory transfer = delayedTransfers[transferId]; ++currentIndex; if (transfer.status != DelayedTransferStatus.COMPLETED) { @@ -616,12 +650,19 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { delayedTransfers[transferId].status = DelayedTransferStatus.COMPLETED; } retrieved = true; - _transfer(transfer.token, transfer.receiver, transfer.amount); + try + this.doTransfer(transfer.token, transfer.receiver, transfer.amount) + // solhint-disable-next-line no-empty-blocks + {} + catch { + emit TransferSkipped(transferId); + } } } else { // status is COMPLETED if (currentIndex == 1) { --currentIndex; + retrieved = true; _removeOldestDelayedTransfer(receiver); } } @@ -669,11 +710,11 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { /** * @dev Allows DepositBoxERC20 to receive ERC20 tokens. - * + * * Emits an {ERC20TokenReady} event. - * + * * Requirements: - * + * * - Amount must be less than or equal to the total supply of the ERC20 contract. * - Whitelist should be turned off for auto adding tokens to DepositBoxERC20. */ @@ -687,7 +728,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { returns (bytes memory data) { bytes32 schainHash = _schainHash(schainName); - ERC20Upgradeable erc20 = ERC20Upgradeable(erc20OnMainnet); + IERC20MetadataUpgradeable erc20 = IERC20MetadataUpgradeable(erc20OnMainnet); uint256 totalSupply = erc20.totalSupply(); require(amount <= totalSupply, "Amount is incorrect"); bool isERC20AddedToSchain = _schainToERC20[schainHash].contains(erc20OnMainnet); @@ -714,11 +755,11 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { /** * @dev Adds an ERC20 token to DepositBoxERC20. - * + * * Emits an {ERC20TokenAdded} event. - * + * * Requirements: - * + * * - Given address should be contract. */ function _addERC20ForSchain(string calldata schainName, address erc20OnMainnet) private { @@ -749,9 +790,9 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { * the element is added at back - depthLimit index */ function _addToDelayedQueueWithPriority( - DoubleEndedQueueUpgradeable.Bytes32Deque storage queue, - uint256 id, - uint256 until, + DoubleEndedQueueUpgradeable.Bytes32Deque storage queue, + uint256 id, + uint256 until, uint256 depthLimit ) private @@ -769,25 +810,6 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { } } - /** - * @dev Transfer ERC20 token or USDT - */ - function _transfer(address token, address receiver, uint256 amount) private { - // there is no other reliable way to determine USDT - // slither-disable-next-line incorrect-equality - if (token == _USDT_ADDRESS) { - // solhint-disable-next-line no-empty-blocks - try IERC20TransferVoid(token).transfer(receiver, amount) {} catch { - revert("Transfer was failed"); - } - } else { - require( - ERC20Upgradeable(token).transfer(receiver, amount), - "Transfer was failed" - ); - } - } - /** * Create instance of DelayedTransfer and initialize all auxiliary fields. */ @@ -817,7 +839,7 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { function _removeOldestDelayedTransfer(address receiver) private { uint256 transferId = uint256(delayedTransfersByReceiver[receiver].popFront()); // For most cases the loop will have only 1 iteration. - // In worst case the amount of iterations is limited by _QUEUE_PROCESSING_LIMIT + // In worst case the amount of iterations is limited by _QUEUE_PROCESSING_LIMIT // slither-disable-next-line costly-loop delete delayedTransfers[transferId]; } @@ -825,14 +847,18 @@ contract DepositBoxERC20 is DepositBox, IDepositBoxERC20 { /** * @dev Returns total supply of ERC20 token. */ - function _getErc20TotalSupply(ERC20Upgradeable erc20Token) private view returns (uint256) { + function _getErc20TotalSupply(IERC20MetadataUpgradeable erc20Token) private view returns (uint256) { return erc20Token.totalSupply(); } /** * @dev Returns info about ERC20 token such as token name, decimals, symbol. */ - function _getErc20TokenInfo(ERC20Upgradeable erc20Token) private view returns (Messages.Erc20TokenInfo memory) { + function _getErc20TokenInfo(IERC20MetadataUpgradeable erc20Token) + private + view + returns (Messages.Erc20TokenInfo memory) + { return Messages.Erc20TokenInfo({ name: erc20Token.name(), decimals: erc20Token.decimals(), diff --git a/proxy/contracts/mainnet/MessageProxyForMainnet.sol b/proxy/contracts/mainnet/MessageProxyForMainnet.sol index 8120c9540..e2c9ebe84 100644 --- a/proxy/contracts/mainnet/MessageProxyForMainnet.sol +++ b/proxy/contracts/mainnet/MessageProxyForMainnet.sol @@ -37,7 +37,7 @@ import "./CommunityPool.sol"; /** * @title Message Proxy for Mainnet * @dev Runs on Mainnet, contains functions to manage the incoming messages from - * `targetSchainName` and outgoing messages to `fromSchainName`. Every SKALE chain with + * `targetSchainName` and outgoing messages to `fromSchainName`. Every SKALE chain with * IMA is therefore connected to MessageProxyForMainnet. * * Messages from SKALE chains are signed using BLS threshold signatures from the @@ -103,6 +103,20 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro uint256 newValue ); + /** + * @dev Emitted when the schain is paused + */ + event SchainPaused( + bytes32 indexed schainHash + ); + + /** + * @dev Emitted when the schain is resumed + */ + event SchainResumed( + bytes32 indexed schainHash + ); + /** * @dev Emitted when reimbursed contract was added */ @@ -139,9 +153,9 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro /** * @dev Allows `msg.sender` to connect schain with MessageProxyOnMainnet for transferring messages. - * + * * Requirements: - * + * * - Schain name must not be `Mainnet`. */ function addConnectedChain(string calldata schainName) external override { @@ -154,9 +168,9 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro /** * @dev Allows owner of the contract to set CommunityPool address for gas reimbursement. - * + * * Requirements: - * + * * - `msg.sender` must be granted as DEFAULT_ADMIN_ROLE. * - Address of CommunityPool contract must not be null. */ @@ -168,9 +182,9 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro /** * @dev Allows `msg.sender` to register extra contract for being able to transfer messages from custom contracts. - * + * * Requirements: - * + * * - `msg.sender` must be granted as EXTRA_CONTRACT_REGISTRAR_ROLE. * - Schain name must not be `Mainnet`. */ @@ -181,16 +195,16 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro isSchainOwner(msg.sender, schainHash), "Not enough permissions to register extra contract" ); - require(schainHash != MAINNET_HASH, "Schain hash can not be equal Mainnet"); + require(schainHash != MAINNET_HASH, "Schain hash can not be equal Mainnet"); _registerExtraContract(schainHash, extraContract); } /** * @dev Allows `msg.sender` to remove extra contract, * thus `extraContract` will no longer be available to transfer messages from mainnet to schain. - * + * * Requirements: - * + * * - `msg.sender` must be granted as EXTRA_CONTRACT_REGISTRAR_ROLE. * - Schain name must not be `Mainnet`. */ @@ -256,10 +270,10 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro } /** - * @dev Posts incoming message from `fromSchainName`. - * + * @dev Posts incoming message from `fromSchainName`. + * * Requirements: - * + * * - `msg.sender` must be authorized caller. * - `fromSchainName` must be initialized. * - `startingCounter` must be equal to the chain's incoming message counter. @@ -290,7 +304,7 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro fromSchainName, _hashedArray(messages, startingCounter, fromSchainName), sign), "Signature is not verified"); - uint additionalGasPerMessage = + uint additionalGasPerMessage = (gasTotal - gasleft() + headerMessageGasCost + messages.length * messageGasCost) / messages.length; uint notReimbursedGas = 0; connectedChains[fromSchainHash].incomingMessageCounter += messages.length; @@ -315,9 +329,9 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro /** * @dev Sets headerMessageGasCost to a new value. - * + * * Requirements: - * + * * - `msg.sender` must be granted as CONSTANT_SETTER_ROLE. */ function setNewHeaderMessageGasCost(uint256 newHeaderMessageGasCost) external override onlyConstantSetter { @@ -327,9 +341,9 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro /** * @dev Sets messageGasCost to a new value. - * + * * Requirements: - * + * * - `msg.sender` must be granted as CONSTANT_SETTER_ROLE. */ function setNewMessageGasCost(uint256 newMessageGasCost) external override onlyConstantSetter { @@ -339,9 +353,9 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro /** * @dev Sets new version of contracts on mainnet - * + * * Requirements: - * + * * - `msg.sender` must be granted DEFAULT_ADMIN_ROLE. */ function setVersion(string calldata newVersion) external override { @@ -352,9 +366,9 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro /** * @dev Allows PAUSABLE_ROLE to pause IMA bridge unlimited - * + * * Requirements: - * + * * - IMA bridge to current schain was not paused * - Sender should be PAUSABLE_ROLE */ @@ -363,13 +377,14 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro require(hasRole(PAUSABLE_ROLE, msg.sender), "Incorrect sender"); require(!pauseInfo[schainHash].paused, "Already paused"); pauseInfo[schainHash].paused = true; + emit SchainPaused(schainHash); } /** - * @dev Allows DEFAULT_ADMIN_ROLE or schain owner to resume IMA bridge - * + * @dev Allows DEFAULT_ADMIN_ROLE or schain owner to resume IMA bridge + * * Requirements: - * + * * - IMA bridge to current schain was paused * - Sender should be DEFAULT_ADMIN_ROLE or schain owner */ @@ -378,6 +393,7 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender) || isSchainOwner(msg.sender, schainHash), "Incorrect sender"); require(pauseInfo[schainHash].paused, "Already unpaused"); pauseInfo[schainHash].paused = false; + emit SchainResumed(schainHash); } /** @@ -440,12 +456,12 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro /** * @dev Checks whether chain is currently connected. - * - * Note: Mainnet chain does not have a public key, and is implicitly + * + * Note: Mainnet chain does not have a public key, and is implicitly * connected to MessageProxy. - * + * * Requirements: - * + * * - `schainName` must not be Mainnet. */ function isConnectedChain( @@ -484,7 +500,7 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro || isContractRegistered(targetChainHash, msg.sender) || isSchainOwner(msg.sender, targetChainHash), "Sender contract is not registered" - ); + ); } /** @@ -514,7 +530,7 @@ contract MessageProxyForMainnet is SkaleManagerClient, MessageProxy, IMessagePro } /** - * @dev Checks whether balance of schain wallet is sufficient for + * @dev Checks whether balance of schain wallet is sufficient for * for reimbursement custom message. */ function _checkSchainBalance(bytes32 schainHash) internal view returns (bool) { diff --git a/proxy/contracts/test/erc20/ERC20IncorrectTransfer.sol b/proxy/contracts/test/erc20/ERC20IncorrectTransfer.sol new file mode 100644 index 000000000..2e1fe4b7d --- /dev/null +++ b/proxy/contracts/test/erc20/ERC20IncorrectTransfer.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +pragma solidity 0.8.16; + +import "./ERC20WithoutTransfer.sol"; + +interface IERC20IncorrectTransfer is IERC20WithoutTransfer { + function transferFrom(address sender, address recipient, uint256 amount, bytes memory) external; +} + + +contract ERC20IncorrectTransfer is IERC20IncorrectTransfer, ERC20WithoutTransfer { + + // solhint-disable-next-line no-empty-blocks + constructor(string memory tokenName, string memory tokenSymbol) ERC20WithoutTransfer(tokenName, tokenSymbol) {} + + function transferFrom(address sender, address recipient, uint256 amount, bytes memory) public override { + _transfer(sender, recipient, amount); + _approve( + sender, + msg.sender, + allowance(sender, msg.sender) - amount + ); + } +} diff --git a/proxy/contracts/test/erc20/ERC20TransferWithFalseReturn.sol b/proxy/contracts/test/erc20/ERC20TransferWithFalseReturn.sol new file mode 100644 index 000000000..1dc3779fc --- /dev/null +++ b/proxy/contracts/test/erc20/ERC20TransferWithFalseReturn.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +pragma solidity 0.8.16; + +import "./ERC20WithoutTransfer.sol"; + +interface IERC20TransferWithFalseReturn is IERC20WithoutTransfer { + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); +} + +contract ERC20TransferWithFalseReturn is IERC20TransferWithFalseReturn, ERC20WithoutTransfer { + + // solhint-disable-next-line no-empty-blocks + constructor(string memory tokenName, string memory tokenSymbol) ERC20WithoutTransfer(tokenName, tokenSymbol) {} + + function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) { + _transfer(sender, recipient, amount); + _approve( + sender, + msg.sender, + allowance(sender, msg.sender) - amount + ); + return false; + } +} diff --git a/proxy/contracts/test/erc20/ERC20TransferWithoutReturn.sol b/proxy/contracts/test/erc20/ERC20TransferWithoutReturn.sol new file mode 100644 index 000000000..4ef8fe0ee --- /dev/null +++ b/proxy/contracts/test/erc20/ERC20TransferWithoutReturn.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +pragma solidity 0.8.16; + +import "./ERC20WithoutTransfer.sol"; + +interface IERC20TransferWithoutReturn is IERC20WithoutTransfer { + function transferFrom(address sender, address recipient, uint256 amount) external; +} + +contract ERC20TransferWithoutReturn is IERC20TransferWithoutReturn, ERC20WithoutTransfer { + + // solhint-disable-next-line no-empty-blocks + constructor(string memory tokenName, string memory tokenSymbol) ERC20WithoutTransfer(tokenName, tokenSymbol) {} + + function transferFrom(address sender, address recipient, uint256 amount) public override { + _transfer(sender, recipient, amount); + _approve( + sender, + msg.sender, + allowance(sender, msg.sender) - amount + ); + } +} diff --git a/proxy/contracts/test/erc20/ERC20WithoutTransfer.sol b/proxy/contracts/test/erc20/ERC20WithoutTransfer.sol new file mode 100644 index 000000000..746fbaa00 --- /dev/null +++ b/proxy/contracts/test/erc20/ERC20WithoutTransfer.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +pragma solidity 0.8.16; + +interface IERC20WithoutTransfer { + function mint(address account, uint256 amount) external returns (bool); + function burn(uint256 amount) external; + function transfer(address recipient, uint256 amount) external returns (bool); + function approve(address spender, uint256 amount) external returns (bool); + function increaseAllowance(address spender, uint256 addedValue) external returns (bool); + function decreaseAllowance(address spender, uint256 subtractedValue) external returns (bool); + function name() external view returns (string memory); + function decimals() external view returns (uint8); + function symbol() external view returns (string memory); + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} + +abstract contract ERC20WithoutTransfer is IERC20WithoutTransfer { + + mapping (address => uint256) private _balances; + + mapping (address => mapping (address => uint256)) private _allowances; + + uint256 private _totalSupply; + + string private _name; + string private _symbol; + uint8 private _decimals; + + event Approval(address owner, address spender, uint256 amount); + + event Transfer(address from, address to, uint256 amount); + + constructor(string memory tokenName, string memory tokenSymbol) { + _name = tokenName; + _symbol = tokenSymbol; + _decimals = 18; + } + + function mint(address account, uint256 amount) external override returns (bool) { + _mint(account, amount); + return true; + } + + /** + * @dev burn - destroys token on msg sender + * + * NEED TO HAVE THIS FUNCTION ON SKALE-CHAIN + * + * @param amount - amount of tokens + */ + function burn(uint256 amount) external override { + _burn(msg.sender, amount); + } + + function transfer(address recipient, uint256 amount) public override returns (bool) { + _transfer(msg.sender, recipient, amount); + return true; + } + + function approve(address spender, uint256 amount) public override returns (bool) { + _approve(msg.sender, spender, amount); + return true; + } + + + function increaseAllowance(address spender, uint256 addedValue) public override returns (bool) { + _approve(msg.sender, spender, _allowances[msg.sender][spender] + addedValue); + return true; + } + + function decreaseAllowance(address spender, uint256 subtractedValue) public override returns (bool) { + _approve( + msg.sender, + spender, + _allowances[msg.sender][spender] - subtractedValue + ); + return true; + } + + function name() public view override returns (string memory) { + return _name; + } + + function symbol() public view override returns (string memory) { + return _symbol; + } + + function decimals() public view override returns (uint8) { + return _decimals; + } + + function totalSupply() public view override returns (uint256) { + return _totalSupply; + } + + function balanceOf(address account) public view override returns (uint256) { + return _balances[account]; + } + + function allowance(address owner, address spender) public view override returns (uint256) { + return _allowances[owner][spender]; + } + + function _transfer(address sender, address recipient, uint256 amount) internal virtual { + require(sender != address(0), "ERC20: transfer from the zero address"); + require(recipient != address(0), "ERC20: transfer to the zero address"); + + _balances[sender] = _balances[sender] - amount; + _balances[recipient] = _balances[recipient] + amount; + emit Transfer(sender, recipient, amount); + } + + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _totalSupply = _totalSupply + amount; + _balances[account] = _balances[account] + amount; + emit Transfer(address(0), account, amount); + } + + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _balances[account] = _balances[account] - amount; + _totalSupply = _totalSupply - amount; + emit Transfer(account, address(0), amount); + } + + function _approve(address owner, address spender, uint256 amount) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } +} diff --git a/proxy/contracts/test/erc20/RevertableERC20.sol b/proxy/contracts/test/erc20/RevertableERC20.sol new file mode 100644 index 000000000..beb3f94b8 --- /dev/null +++ b/proxy/contracts/test/erc20/RevertableERC20.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +/** + * RevertableERC20.sol - SKALE Interchain Messaging Agent + * Copyright (C) 2022-Present SKALE Labs + * @author Dmytro Stebaiev + * + * SKALE IMA is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SKALE IMA is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with SKALE IMA. If not, see . + */ + +pragma solidity 0.8.16; + +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; + +interface IRevertableERC20 { + function enable() external; + function disable() external; + function mint(address account, uint amount) external; +} + +contract RevertableERC20 is IRevertableERC20, ERC20Upgradeable { + bool public enabled = true; + + constructor(string memory name, string memory symbol) initializer { + super.__ERC20_init(name, symbol); + } + + function enable() external override { + enabled = true; + } + + function disable() external override { + enabled = false; + } + + function mint(address account, uint amount) external override { + _mint(account, amount); + } + + function _transfer( + address from, + address to, + uint256 amount + ) + internal + override + { + require(enabled, "Transfers are disabled"); + super._transfer(from, to, amount); + } +} \ No newline at end of file diff --git a/proxy/migrations/tools/gnosis-safe.ts b/proxy/migrations/tools/gnosis-safe.ts index b4938e319..1e5013f79 100644 --- a/proxy/migrations/tools/gnosis-safe.ts +++ b/proxy/migrations/tools/gnosis-safe.ts @@ -9,6 +9,7 @@ type Ethers = typeof ethers & HardhatEthersHelpers; enum Network { MAINNET = 1, RINKEBY = 4, + GOERLI = 5, GANACHE = 1337, HARDHAT = 31337, } @@ -19,6 +20,7 @@ const ADDRESSES = { multiSend: { [Network.MAINNET]: "0x8D29bE29923b68abfDD21e541b9374737B49cdAD", [Network.RINKEBY]: "0x8D29bE29923b68abfDD21e541b9374737B49cdAD", + [Network.GOERLI]: "0x8D29bE29923b68abfDD21e541b9374737B49cdAD", }, } @@ -26,10 +28,12 @@ const URLS = { safe_transaction: { [Network.MAINNET]: "https://safe-transaction.mainnet.gnosis.io", [Network.RINKEBY]: "https://safe-transaction.rinkeby.gnosis.io", + [Network.GOERLI]: "https://safe-transaction.goerli.gnosis.io", }, safe_relay: { [Network.MAINNET]: "https://safe-relay.mainnet.gnosis.io", [Network.RINKEBY]: "https://safe-relay.rinkeby.gnosis.io", + [Network.GOERLI]: "https://safe-relay.goerli.gnosis.io", } } @@ -40,6 +44,8 @@ function getMultiSendAddress(chainId: number, isSafeMock: boolean = false) { return ADDRESSES.multiSend[chainId]; } else if (chainId === Network.RINKEBY) { return ADDRESSES.multiSend[chainId]; + } else if (chainId === Network.GOERLI) { + return ADDRESSES.multiSend[chainId]; } else if ([Network.GANACHE, Network.HARDHAT].includes(chainId)) { return ethers.constants.AddressZero; } else { @@ -50,7 +56,9 @@ function getMultiSendAddress(chainId: number, isSafeMock: boolean = false) { export function getSafeTransactionUrl(chainId: number) { if (chainId === Network.MAINNET) { return URLS.safe_transaction[chainId]; - } else if (chainId === 4) { + } else if (chainId === Network.RINKEBY) { + return URLS.safe_transaction[chainId]; + } else if (chainId === Network.GOERLI) { return URLS.safe_transaction[chainId]; } else { throw Error("Can't get safe-transaction url at network with chainId = " + chainId); @@ -58,10 +66,8 @@ export function getSafeTransactionUrl(chainId: number) { } export function getSafeRelayUrl(chainId: number) { - if (chainId === 1) { - return URLS.safe_relay[chainId]; - } else if (chainId === 4) { - return URLS.safe_relay[chainId]; + if (Object.keys(URLS.safe_relay).includes(chainId.toString())) { + return URLS.safe_relay[chainId as keyof typeof URLS.safe_relay]; } else { throw Error("Can't get safe-relay url at network with chainId = " + chainId); } diff --git a/proxy/migrations/tools/verification.ts b/proxy/migrations/tools/verification.ts index bc37440ff..6373c6741 100644 --- a/proxy/migrations/tools/verification.ts +++ b/proxy/migrations/tools/verification.ts @@ -12,7 +12,7 @@ export async function verify(contractName: string, contractAddress: string, cons }); break; } catch (e: any) { - if (e.toString().includes("Already Verified")) { + if (e.toString().includes("Contract source code already verified")) { console.log(chalk.grey(`${contractName} is already verified`)); return; } diff --git a/proxy/migrations/upgradeMainnet.ts b/proxy/migrations/upgradeMainnet.ts index 1cdb09380..3ca1967c1 100644 --- a/proxy/migrations/upgradeMainnet.ts +++ b/proxy/migrations/upgradeMainnet.ts @@ -9,75 +9,10 @@ import hre from "hardhat"; async function main() { await upgrade( - "1.3.2", + "1.3.4", contracts, async (safeTransactions, abi) => undefined, - async (safeTransactions, abi) => { - const proxyAdmin = await getManifestAdmin(hre); - const owner = await proxyAdmin.owner(); - const communityPoolName = "CommunityPool"; - const communityPoolFactory = await ethers.getContractFactory(communityPoolName); - const communityPoolAddress = abi[getContractKeyInAbiFile(communityPoolName) + "_address"]; - let communityPool; - if (communityPoolAddress) { - communityPool = communityPoolFactory.attach(communityPoolAddress) as CommunityPool; - const constantSetterRole = await communityPool.CONSTANT_SETTER_ROLE(); - const isHasRole = await communityPool.hasRole(constantSetterRole, owner); - if (!isHasRole) { - console.log(chalk.yellow("Prepare transaction to grantRole CONSTANT_SETTER_ROLE to " + owner)); - safeTransactions.push(encodeTransaction( - 0, - communityPoolAddress, - 0, - communityPool.interface.encodeFunctionData("grantRole", [constantSetterRole, owner]) - )); - } - console.log(chalk.yellow("Prepare transaction to set multiplier to 3/2")); - safeTransactions.push(encodeTransaction( - 0, - communityPoolAddress, - 0, - communityPool.interface.encodeFunctionData("setMultiplier", [3, 2]) - )); - console.log(chalk.yellow("Prepare transaction to set header message gas cost to 73800")); - } else { - console.log(chalk.red("CommunityPool was not found!")); - console.log(chalk.red("Check your abi!!!")); - process.exit(1); - } - - const messageProxyForMainnet = (await ethers.getContractFactory("MessageProxyForMainnet")) - .attach(abi[getContractKeyInAbiFile("MessageProxyForMainnet") + "_address"]) as MessageProxyForMainnet; - - if (! await messageProxyForMainnet.hasRole(await messageProxyForMainnet.CONSTANT_SETTER_ROLE(), owner)) { - console.log(chalk.yellow("Prepare transaction to grantRole CONSTANT_SETTER_ROLE to " + owner)); - safeTransactions.push(encodeTransaction( - 0, - messageProxyForMainnet.address, - 0, - messageProxyForMainnet.interface.encodeFunctionData( - "grantRole", - [ await messageProxyForMainnet.CONSTANT_SETTER_ROLE(), owner ] - ) - )); - } - - const newHeaderMessageGasCost = 92251; - - console.log(chalk.yellow( - "Prepare transaction to set header message gas cost to", - newHeaderMessageGasCost.toString() - )); - safeTransactions.push(encodeTransaction( - 0, - messageProxyForMainnet.address, - 0, - messageProxyForMainnet.interface.encodeFunctionData( - "setNewHeaderMessageGasCost", - [ newHeaderMessageGasCost ] - ) - )); - }, + async (safeTransactions, abi) => undefined, "proxyMainnet" ); } diff --git a/proxy/migrations/upgradeSchain.ts b/proxy/migrations/upgradeSchain.ts index 6f25a1d69..72ac293fc 100644 --- a/proxy/migrations/upgradeSchain.ts +++ b/proxy/migrations/upgradeSchain.ts @@ -19,7 +19,7 @@ async function main() { const pathToManifest: string = stringValue(process.env.MANIFEST); await manifestSetup( pathToManifest ); await upgrade( - "1.3.2", + "1.3.4", contracts, async (safeTransactions, abi) => { // deploying of new contracts diff --git a/proxy/package.json b/proxy/package.json index 39716805a..0f94cda51 100644 --- a/proxy/package.json +++ b/proxy/package.json @@ -26,8 +26,8 @@ "@openzeppelin/contracts-upgradeable": "^4.7.1", "@openzeppelin/hardhat-upgrades": "^1.9.0", "@skalenetwork/etherbase-interfaces": "^0.0.1-develop.20", - "@skalenetwork/ima-interfaces": "1.0.0-develop.24", - "@skalenetwork/skale-manager-interfaces": "1.0.0", + "@skalenetwork/ima-interfaces": "^1.1.0-develop.0", + "@skalenetwork/skale-manager-interfaces": "1.0.0-develop.1", "axios": "^0.21.4", "dotenv": "^10.0.0", "ethereumjs-tx": "2.1.2", diff --git a/proxy/test/DepositBoxERC20.ts b/proxy/test/DepositBoxERC20.ts index c946af4ac..6b37c51ad 100644 --- a/proxy/test/DepositBoxERC20.ts +++ b/proxy/test/DepositBoxERC20.ts @@ -62,6 +62,7 @@ import { BigNumber, Wallet } from "ethers"; import { assert, expect, use } from "chai"; import { createNode } from "./utils/skale-manager-utils/nodes"; import { currentTime, skipTime } from "./utils/time"; +import { join } from "path"; const BlsSignature: [BigNumber, BigNumber] = [ BigNumber.from("178325537405109593276798394634841698946852714038246117383766698579865918287"), @@ -233,6 +234,39 @@ describe("DepositBoxERC20", () => { await depositBoxERC20.connect(schainOwner).addERC20TokenByOwner(schainName, erc20.address) .should.be.eventually.rejectedWith("Schain is killed"); }); + + it("should invoke `depositERC20` for non standard ERC20", async () => { + // preparation + // mint some quantity of ERC20 tokens for `deployer` address + const erc20TWR = await (await ethers.getContractFactory("ERC20TransferWithoutReturn")).deploy("Test", "TST"); + const erc20TWFR = await (await ethers.getContractFactory("ERC20TransferWithFalseReturn")).deploy("Test", "TST"); + const erc20IT = await (await ethers.getContractFactory("ERC20IncorrectTransfer")).deploy("Test", "TST"); + const amount = 10; + await erc20TWR.connect(deployer).mint(deployer.address, amount); + await erc20TWFR.connect(deployer).mint(deployer.address, amount); + await erc20IT.connect(deployer).mint(deployer.address, amount); + await erc20.connect(deployer).mint(deployer.address, amount); + await erc20TWR.connect(deployer).approve(depositBoxERC20.address, amount); + await erc20TWFR.connect(deployer).approve(depositBoxERC20.address, amount); + await erc20IT.connect(deployer).approve(depositBoxERC20.address, amount); + await erc20.connect(deployer).approve(depositBoxERC20.address, amount); + // execution + await depositBoxERC20.connect(schainOwner).disableWhitelist(schainName); + await depositBoxERC20 + .connect(deployer) + .depositERC20(schainName, erc20.address, 1); + await depositBoxERC20 + .connect(deployer) + .depositERC20(schainName, erc20TWR.address, 1); + await depositBoxERC20 + .connect(deployer) + .depositERC20(schainName, erc20IT.address, 1) + .should.be.eventually.rejectedWith("SafeERC20: low-level call failed"); + await depositBoxERC20 + .connect(deployer) + .depositERC20(schainName, erc20TWFR.address, 1) + .should.be.eventually.rejectedWith("SafeERC20: ERC20 operation did not succeed"); + }); }); describe("tests for `postMessage` function", async () => { @@ -330,6 +364,73 @@ describe("DepositBoxERC20", () => { }); + it("should transfer non standard ERC20 token", async () => { + // preparation + const erc20TWR = await (await ethers.getContractFactory("ERC20TransferWithoutReturn")).deploy("Test", "TST"); + const amount = 10; + const to = user.address; + const senderFromSchain = deployer.address; + const wei = 1e18.toString(); + + const sign = { + blsSignature: BlsSignature, + counter: Counter, + hashA: HashA, + hashB: HashB, + }; + + const message = { + data: await messages.encodeTransferErc20Message(erc20.address, to, amount), + destinationContract: depositBoxERC20.address, + sender: senderFromSchain + }; + const messageTWR = { + data: await messages.encodeTransferErc20Message(erc20TWR.address, to, amount), + destinationContract: depositBoxERC20.address, + sender: senderFromSchain + }; + + await initializeSchain(contractManager, schainName, schainOwner.address, 1, 1); + await setCommonPublicKey(contractManager, schainName); + + await depositBoxERC20.connect(user).depositERC20(schainName, erc20.address, amount) + .should.be.eventually.rejectedWith("Unconnected chain"); + + await linker + .connect(deployer) + .connectSchain(schainName, [deployer.address, deployer.address, deployer.address]); + + await communityPool + .connect(user) + .rechargeUserWallet(schainName, user.address, { value: wei }); + + await depositBoxERC20.connect(schainOwner).disableWhitelist(schainName); + await erc20.connect(deployer).mint(user.address, amount * 2); + await erc20TWR.connect(deployer).mint(user.address, amount * 2); + + await erc20.connect(user).approve(depositBoxERC20.address, amount * 2); + await erc20TWR.connect(user).approve(depositBoxERC20.address, amount * 2); + + await depositBoxERC20.connect(user).depositERC20(schainName, erc20.address, amount); + await depositBoxERC20.connect(user).depositERC20(schainName, erc20TWR.address, amount); + + const balanceBefore = await deployer.getBalance(); + await messageProxy.connect(nodeAddress).postIncomingMessages(schainName, 0, [message, messageTWR], sign); + const balance = await deployer.getBalance(); + balance.should.be.least(balanceBefore); + balance.should.be.closeTo(balanceBefore, 10); + + await depositBoxERC20.connect(user).depositERC20(schainName, erc20.address, amount); + await depositBoxERC20.connect(user).depositERC20(schainName, erc20TWR.address, amount); + await messageProxy.connect(nodeAddress).postIncomingMessages(schainName, 2, [message, messageTWR], sign); + expect(BigNumber.from(await depositBoxERC20.transferredAmount(schainHash, erc20.address)).toString()).to.be.equal(BigNumber.from(0).toString()); + expect(BigNumber.from(await depositBoxERC20.transferredAmount(schainHash, erc20TWR.address)).toString()).to.be.equal(BigNumber.from(0).toString()); + + (await erc20.balanceOf(user.address)).toString().should.be.equal((amount * 2).toString()); + (await erc20TWR.balanceOf(user.address)).toString().should.be.equal((amount * 2).toString()); + + }); + describe("When user deposited tokens", async () => { let token: ERC20OnChain; let token2: ERC20OnChain; @@ -370,10 +471,19 @@ describe("DepositBoxERC20", () => { await token2.connect(user).approve(depositBoxERC20.address, depositedAmount); await depositBoxERC20.connect(user).depositERC20(schainName, token2.address, depositedAmount); - await depositBoxERC20.connect(schainOwner).setBigTransferValue(schainName, token.address, bigAmount); + await expect( + depositBoxERC20.connect(schainOwner).setBigTransferValue(schainName, token.address, bigAmount) + ).to.emit(depositBoxERC20, "BigTransferThresholdIsChanged") + .withArgs(schainHash, token.address, 0, bigAmount); await depositBoxERC20.connect(schainOwner).setBigTransferValue(schainName, token2.address, bigAmount); - await depositBoxERC20.connect(schainOwner).setBigTransferDelay(schainName, timeDelay); - await depositBoxERC20.connect(schainOwner).setArbitrageDuration(schainName, arbitrageDuration); + await expect( + depositBoxERC20.connect(schainOwner).setBigTransferDelay(schainName, timeDelay) + ).to.emit(depositBoxERC20, "BigTransferDelayIsChanged") + .withArgs(schainHash, 0, timeDelay); + await expect( + depositBoxERC20.connect(schainOwner).setArbitrageDuration(schainName, arbitrageDuration) + ).to.emit(depositBoxERC20, "ArbitrageDurationIsChanged") + .withArgs(schainHash, 0, arbitrageDuration); await depositBoxERC20.grantRole(await depositBoxERC20.ARBITER_ROLE(), deployer.address); }); @@ -512,6 +622,94 @@ describe("DepositBoxERC20", () => { .should.be.equal(token1BalanceBefore.add(2 * amount + 3 * bigAmount)); }); + it("should not stuck after big amount of competed transfers", async () => { + const bigTransfer = { + data: await messages.encodeTransferErc20Message(token.address, user.address, bigAmount), + destinationContract: depositBoxERC20.address, + sender: deployer.address + }; + + const token1BalanceBefore = await token.balanceOf(user.address); + const amountOfCompetedTransfers = 15; + + // send `amountOfCompetedTransfers` + 1 big transfer + const batch = (await messageProxy.MESSAGES_LENGTH()).toNumber(); + const fullBatches = Math.floor((amountOfCompetedTransfers + 1) / batch); + const rest = amountOfCompetedTransfers + 1 - fullBatches * batch; + for (let i = 0; i < fullBatches; ++i) { + await messageProxy.connect(nodeAddress).postIncomingMessages( + schainName, + i * batch, + Array(batch).fill(bigTransfer), + randomSignature + ); + } + if (rest > 0) { + await messageProxy.connect(nodeAddress).postIncomingMessages( + schainName, + fullBatches * batch, + Array(rest).fill(bigTransfer), + randomSignature + ); + } + + (await token.balanceOf(user.address)).should.be.equal(token1BalanceBefore); + + for (const completedTransfer of [...Array(amountOfCompetedTransfers).keys()]) { + await depositBoxERC20.escalate(completedTransfer); + await depositBoxERC20.connect(schainOwner).validateTransfer(completedTransfer); + } + + (await token.balanceOf(user.address)).should.be.equal(token1BalanceBefore.add(bigAmount * amountOfCompetedTransfers)); + + await skipTime(timeDelay); + + // first retrieve removes already completed transfers after an arbitrage from the queue + await depositBoxERC20.retrieveFor(user.address); + (await token.balanceOf(user.address)).should.be.equal(token1BalanceBefore.add(bigAmount * amountOfCompetedTransfers)); + + // second retrieve withdraws the rest + await depositBoxERC20.retrieveFor(user.address); + (await token.balanceOf(user.address)).should.be.equal(token1BalanceBefore.add(bigAmount * (amountOfCompetedTransfers + 1))); + + }); + + it("should not stuck if a token reverts transfer", async () => { + const bigTransfer = { + data: await messages.encodeTransferErc20Message(token.address, user.address, bigAmount), + destinationContract: depositBoxERC20.address, + sender: deployer.address + }; + + const badToken = await (await ethers.getContractFactory("RevertableERC20")).deploy("Test", "TST"); + await badToken.mint(user.address, bigAmount); + await badToken.connect(user).approve(depositBoxERC20.address, bigAmount); + await depositBoxERC20.connect(user).depositERC20(schainName, badToken.address, bigAmount); + + const badTokenBigTransfer = { + data: await messages.encodeTransferErc20Message(badToken.address, user.address, bigAmount), + destinationContract: depositBoxERC20.address, + sender: deployer.address + }; + + await messageProxy.connect(nodeAddress).postIncomingMessages( + schainName, + 0, + [ badTokenBigTransfer, bigTransfer ], + randomSignature + ); + + await skipTime(timeDelay); + + await badToken.disable(); + const balanceBefore = await token.balanceOf(user.address); + await expect( + depositBoxERC20.retrieveFor(user.address) + ).to.emit(depositBoxERC20, "TransferSkipped") + .withArgs(0); + (await token.balanceOf(user.address)).should.be.equal(balanceBefore.add(bigAmount)); + }); + it("should not allow to set too big delays", async () => { const tenYears = Math.round(60 * 60 * 24 * 365.25 * 10) diff --git a/proxy/test/MessageProxy.ts b/proxy/test/MessageProxy.ts index 57393830c..6d613f821 100644 --- a/proxy/test/MessageProxy.ts +++ b/proxy/test/MessageProxy.ts @@ -283,7 +283,10 @@ describe("MessageProxy", () => { const pauseableRole = await messageProxyForMainnet.PAUSABLE_ROLE(); await messageProxyForMainnet.connect(deployer).grantRole(pauseableRole, client.address); - await messageProxyForMainnet.connect(client).pause(schainName); + await expect( + messageProxyForMainnet.connect(client).pause(schainName) + ).to.emit(messageProxyForMainnet, "SchainPaused") + .withArgs(schainHash); await messageProxyForMainnet.connect(client).pause(schainName).should.be.rejectedWith("Already paused"); (await messageProxyForMainnet.isPaused(schainHash)).should.be.deep.equal(true); @@ -296,7 +299,10 @@ describe("MessageProxy", () => { .should.be.rejectedWith("IMA is paused"); await messageProxyForMainnet.connect(client).resume(schainName).should.be.rejectedWith("Incorrect sender"); - await messageProxyForMainnet.connect(schainOwner).resume(schainName); + await expect( + messageProxyForMainnet.connect(schainOwner).resume(schainName) + ).to.emit(messageProxyForMainnet, "SchainResumed") + .withArgs(schainHash); await messageProxyForMainnet.connect(deployer).resume(schainName).should.be.rejectedWith("Already unpaused"); (await messageProxyForMainnet.isPaused(schainHash)).should.be.deep.equal(false); diff --git a/proxy/yarn.lock b/proxy/yarn.lock index 3c930d361..4646476f0 100644 --- a/proxy/yarn.lock +++ b/proxy/yarn.lock @@ -757,17 +757,17 @@ resolved "https://registry.yarnpkg.com/@skalenetwork/etherbase-interfaces/-/etherbase-interfaces-0.0.1-develop.20.tgz#33f61e18d695fd47063aa39dce4df335d26b9528" integrity sha512-j3xnuQtOtjvjAoUMJgSUFxRa9/Egkg1RyA8r6PjcEb33VksE4LWLBy0PNFUFehLZv48595JROTcViGeXXwg5HQ== -"@skalenetwork/ima-interfaces@1.0.0-develop.24": - version "1.0.0-develop.24" - resolved "https://registry.yarnpkg.com/@skalenetwork/ima-interfaces/-/ima-interfaces-1.0.0-develop.24.tgz#20247612c602a90b6f5e4e0cce6e3eb8e64f2ed1" - integrity sha512-qVkcg4DBtH7lbVzTO6AcFILnvOTPesImyVvWqMlyzPOvfj4qUsXeqQqE6pDbhbWev68Ar7cvqWmfK+eEENp1pw== +"@skalenetwork/ima-interfaces@^1.1.0-develop.0": + version "1.1.0-develop.0" + resolved "https://registry.yarnpkg.com/@skalenetwork/ima-interfaces/-/ima-interfaces-1.1.0-develop.0.tgz#a91a794affee2138e03c779f9f32cd34652a47b7" + integrity sha512-wy1DYbsgccYETqgayxyHh9Dh8+q13F0JrDCpHn7nodwnjxp95cAtjyD9KTN4Y4/Ix/pr5rFqQjBXtXQas36uBg== dependencies: "@skalenetwork/skale-manager-interfaces" "^0.1.2" -"@skalenetwork/skale-manager-interfaces@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@skalenetwork/skale-manager-interfaces/-/skale-manager-interfaces-1.0.0.tgz#5456ecbb7e36300544296d24c2345be54c5c9218" - integrity sha512-TOgoQZXbYj5KPZnyzs+1L9g6RykSYxE6/9NnPfb8aDYy4Y5Pp/JIK7cy4qSnXetarCGhIGvNo/HBU8naq6dhEQ== +"@skalenetwork/skale-manager-interfaces@1.0.0-develop.1": + version "1.0.0-develop.1" + resolved "https://registry.yarnpkg.com/@skalenetwork/skale-manager-interfaces/-/skale-manager-interfaces-1.0.0-develop.1.tgz#8a2d06872dcdc235c041448efc454c0d5f1e55cf" + integrity sha512-ZOSWEqvz76Ez5cuWE8n9+j/tnTVQg+t7vaZar/XME0hxnM1YwNFqy34nqjEC/SMhFXg/WnxdOVX9JWWXVZRlkQ== "@skalenetwork/skale-manager-interfaces@^0.1.2": version "0.1.2"