diff --git a/helix-contract/address/arbi2eth-ln-pro.json b/helix-contract/address/arbi2eth-ln-pro.json index b217bd53..5ab35490 100644 --- a/helix-contract/address/arbi2eth-ln-pro.json +++ b/helix-contract/address/arbi2eth-ln-pro.json @@ -11,5 +11,18 @@ "LnBridgeLogic": "0x1928C29b8eCD6c877A2E6bEF01bD50C72E1be0cC", "LnBridgeProxy": "0xFBAD806Bdf9cEC2943be281FB355Da05068DE925", "Ring": "0x9e523234D36973f9e38642886197D023C88e307e" + }, + "arbitrum2ethereumLnV2-ethereum": { + "LnBridgeProxyAdmin": "0x66D86a686e50C98BaC236105eFAFb99Ee7605dc5", + "LnBridgeLogic": "0xd12917F42E09e216623010EB5f15c39d4978d322", + "LnBridgeProxy": "0xeAb1F01a8f4A2687023B159c2063639Adad5304E", + "Ring": "0x9469D013805bFfB7D3DEBe5E7839237e535ec483", + "Inbox": "0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f" + }, + "arbitrum2ethereumLnV2-arbitrum": { + "LnBridgeProxyAdmin": "0x61B6B8c7C00aA7F060a2BEDeE6b11927CC9c3eF1", + "LnBridgeLogic": "0x2FC7Def96561ca5e1f0Aa333cb78cFA51DeE3218", + "LnBridgeProxy": "0xD1B10B114f1975d8BCc6cb6FC43519160e2AA978", + "Ring": "0x9e523234D36973f9e38642886197D023C88e307e" } } diff --git a/helix-contract/contracts/ln/base/LnOppositeBridgeSource.sol b/helix-contract/contracts/ln/base/LnOppositeBridgeSource.sol index effa9410..23151400 100644 --- a/helix-contract/contracts/ln/base/LnOppositeBridgeSource.sol +++ b/helix-contract/contracts/ln/base/LnOppositeBridgeSource.sol @@ -38,6 +38,7 @@ contract LnOppositeBridgeSource is LnBridgeHelper { } struct LnProviderInfo { LnProviderConfigure config; + bool pause; bytes32 lastTransferId; } @@ -100,6 +101,16 @@ contract LnOppositeBridgeSource is LnBridgeHelper { tokenInfos[_token].penaltyLnCollateral = _penaltyLnCollateral; } + function providerPause(address sourceToken) external { + bytes32 providerKey = getProviderKey(msg.sender, sourceToken); + lnProviders[providerKey].pause = true; + } + + function providerUnpause(address sourceToken) external { + bytes32 providerKey = getProviderKey(msg.sender, sourceToken); + lnProviders[providerKey].pause = false; + } + // lnProvider can register or update its configure by using this function // * `margin` is the increased value of the deposited margin function updateProviderFeeAndMargin( @@ -179,6 +190,8 @@ contract LnOppositeBridgeSource is LnBridgeHelper { bytes32 providerKey = getProviderKey(snapshot.provider, snapshot.sourceToken); LnProviderInfo memory providerInfo = lnProviders[providerKey]; + require(!providerInfo.pause, "provider paused"); + TokenInfo memory tokenInfo = tokenInfos[snapshot.sourceToken]; uint256 providerFee = calculateProviderFee(providerInfo.config, amount); diff --git a/helix-contract/deploy/deploy_arbi2eth_ln.js b/helix-contract/deploy/deploy_arbi2eth_ln.js index dfd8b7d5..abab4492 100644 --- a/helix-contract/deploy/deploy_arbi2eth_ln.js +++ b/helix-contract/deploy/deploy_arbi2eth_ln.js @@ -21,6 +21,26 @@ const daoOnEthereum = "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4"; const initTransferId = "0x0000000000000000000000000000000000000000000000000000000000000000"; +async function getLnBridgeTargetInitData(wallet, dao, inbox) { + const bridgeContract = await ethers.getContractFactory("Arb2EthTarget", wallet); + const initdata = await ProxyDeployer.getInitializerData( + bridgeContract.interface, + [dao, inbox], + "initialize", + ); + console.log("LnBridgeInitData init data:", initdata); +} + +async function getLnBridgeSourceInitData(wallet, dao) { + const bridgeContract = await ethers.getContractFactory("Arb2EthSource", wallet); + const initdata = await ProxyDeployer.getInitializerData( + bridgeContract.interface, + [dao], + "initialize", + ); + console.log("LnBridgeInitData init data:", initdata); +} + async function transferAndLockMargin( wallet, bridgeAddress, @@ -294,11 +314,15 @@ async function main() { const arbitrumWallet = wallets[0]; const ethereumWallet = wallets[1]; - + //await getLnBridgeTargetInitData(arbitrumWallet, "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4", "0x4Dbd4fc535Ac27206064B68FfCf827b0A60BAB3f"); + await getLnBridgeSourceInitData(arbitrumWallet, "0x88a39B052d477CfdE47600a7C9950a441Ce61cb4"); + return; + + /* const deployed = await deploy(arbitrumWallet, ethereumWallet); console.log(deployed); return; - + */ const arbitrumLnBridgeAddress = "0x7B8413FA1c1033844ac813A2E6475E15FB0fb3BA"; @@ -384,7 +408,7 @@ async function main() { await requestWithdrawMargin( ethereumWallet, ethereumLnBridgeAddress, - "0xD1207442C3AC4BABC7500E06C2C08E3E5A46A452D92A7936A9B90ECE22C55E5E", //lastTransferId + "0xDD5703D47E4494FFC87660F3CBF2AFBA7A137755A91C81DC7ED120BB18E33A83", //lastTransferId ringArbitrumAddress, ethers.utils.parseEther("3"), // amount ); diff --git a/helix-contract/flatten/lnv2/Arb2EthSource.sol b/helix-contract/flatten/lnv2/Arb2EthSource.sol index 8f07caab..5c0c3fe2 100644 --- a/helix-contract/flatten/lnv2/Arb2EthSource.sol +++ b/helix-contract/flatten/lnv2/Arb2EthSource.sol @@ -14,7 +14,7 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 7/11/2023 + * 7/12/2023 **/ pragma solidity ^0.8.10; @@ -1284,6 +1284,7 @@ contract LnOppositeBridgeSource is LnBridgeHelper { } struct LnProviderInfo { LnProviderConfigure config; + bool pause; bytes32 lastTransferId; } @@ -1346,6 +1347,16 @@ contract LnOppositeBridgeSource is LnBridgeHelper { tokenInfos[_token].penaltyLnCollateral = _penaltyLnCollateral; } + function providerPause(address sourceToken) external { + bytes32 providerKey = getProviderKey(msg.sender, sourceToken); + lnProviders[providerKey].pause = true; + } + + function providerUnpause(address sourceToken) external { + bytes32 providerKey = getProviderKey(msg.sender, sourceToken); + lnProviders[providerKey].pause = false; + } + // lnProvider can register or update its configure by using this function // * `margin` is the increased value of the deposited margin function updateProviderFeeAndMargin( @@ -1425,6 +1436,8 @@ contract LnOppositeBridgeSource is LnBridgeHelper { bytes32 providerKey = getProviderKey(snapshot.provider, snapshot.sourceToken); LnProviderInfo memory providerInfo = lnProviders[providerKey]; + require(!providerInfo.pause, "provider paused"); + TokenInfo memory tokenInfo = tokenInfos[snapshot.sourceToken]; uint256 providerFee = calculateProviderFee(providerInfo.config, amount); diff --git a/helix-contract/flatten/lnv2/Arb2EthTarget.sol b/helix-contract/flatten/lnv2/Arb2EthTarget.sol index 464f563e..2b444a14 100644 --- a/helix-contract/flatten/lnv2/Arb2EthTarget.sol +++ b/helix-contract/flatten/lnv2/Arb2EthTarget.sol @@ -14,137 +14,341 @@ * '----------------' '----------------' '----------------' '----------------' '----------------' ' * * - * 7/11/2023 + * 7/12/2023 **/ pragma solidity ^0.8.10; -// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 +// File contracts/ln/interface/ILnOppositeBridgeSource.sol // License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) -/** - * @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; - } +interface ILnOppositeBridgeSource { + function slash( + bytes32 lastRefundTransferId, + bytes32 transferId, + address provider, + address sourceToken, + address slasher + ) external; - function _msgData() internal view virtual returns (bytes calldata) { - return msg.data; - } + function withdrawMargin( + bytes32 lastRefundTransferId, + bytes32 lastTransferId, + address provider, + address sourceToken, + uint112 amount + ) external; } -// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 +// File @zeppelin-solidity/contracts/token/ERC20/IERC20.sol@v4.7.3 // License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) +// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) /** - * @dev Contract module which allows children to implement an emergency stop - * mechanism that can be triggered by an authorized account. - * - * This module is used through inheritance. It will make available the - * modifiers `whenNotPaused` and `whenPaused`, which can be applied to - * the functions of your contract. Note that they will not be pausable by - * simply including this module, only once the modifiers are put in place. + * @dev Interface of the ERC20 standard as defined in the EIP. */ -abstract contract Pausable is Context { +interface IERC20 { /** - * @dev Emitted when the pause is triggered by `account`. + * @dev Emitted when `value` tokens are moved from one account (`from`) to + * another (`to`). + * + * Note that `value` may be zero. */ - event Paused(address account); + event Transfer(address indexed from, address indexed to, uint256 value); /** - * @dev Emitted when the pause is lifted by `account`. + * @dev Emitted when the allowance of a `spender` for an `owner` is set by + * a call to {approve}. `value` is the new allowance. */ - event Unpaused(address account); + event Approval(address indexed owner, address indexed spender, uint256 value); - bool private _paused; + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); /** - * @dev Initializes the contract in unpaused state. + * @dev Returns the amount of tokens owned by `account`. */ - constructor() { - _paused = false; - } + function balanceOf(address account) external view returns (uint256); /** - * @dev Modifier to make a function callable only when the contract is not paused. + * @dev Moves `amount` tokens from the caller's account to `to`. * - * Requirements: + * Returns a boolean value indicating whether the operation succeeded. * - * - The contract must not be paused. + * Emits a {Transfer} event. */ - modifier whenNotPaused() { - _requireNotPaused(); - _; - } + function transfer(address to, uint256 amount) external returns (bool); /** - * @dev Modifier to make a function callable only when the contract is paused. - * - * Requirements: + * @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. * - * - The contract must be paused. + * This value changes when {approve} or {transferFrom} are called. */ - modifier whenPaused() { - _requirePaused(); - _; - } + function allowance(address owner, address spender) external view returns (uint256); /** - * @dev Returns true if the contract is paused, and false otherwise. + * @dev Sets `amount` 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 paused() public view virtual returns (bool) { - return _paused; - } + function approve(address spender, uint256 amount) external returns (bool); /** - * @dev Throws if the contract is paused. + * @dev Moves `amount` tokens from `from` to `to` using the + * allowance mechanism. `amount` is then deducted from the caller's + * allowance. + * + * Returns a boolean value indicating whether the operation succeeded. + * + * Emits a {Transfer} event. */ - function _requireNotPaused() internal view virtual { - require(!paused(), "Pausable: paused"); + function transferFrom( + address from, + address to, + uint256 amount + ) external returns (bool); +} + +// File contracts/ln/base/LnBridgeHelper.sol +// License-Identifier: MIT + +contract LnBridgeHelper { + bytes32 constant public INIT_SLASH_TRANSFER_ID = bytes32(uint256(1)); + + struct TransferParameter { + bytes32 previousTransferId; + address provider; + address sourceToken; + address targetToken; + uint112 amount; + uint64 timestamp; + address receiver; } - /** - * @dev Throws if the contract is not paused. - */ - function _requirePaused() internal view virtual { - require(paused(), "Pausable: not paused"); + function _safeTransfer( + address token, + address receiver, + uint256 amount + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector( + IERC20.transfer.selector, + receiver, + amount + )); + require(success && (data.length == 0 || abi.decode(data, (bool))), "lnBridgeHelper:transfer token failed"); + } + + function _safeTransferFrom( + address token, + address sender, + address receiver, + uint256 amount + ) internal { + (bool success, bytes memory data) = token.call(abi.encodeWithSelector( + IERC20.transferFrom.selector, + sender, + receiver, + amount + )); + require(success && (data.length == 0 || abi.decode(data, (bool))), "lnBridgeHelper:transferFrom token failed"); + } + + function getProviderKey(address provider, address token) pure public returns(bytes32) { + return keccak256(abi.encodePacked( + provider, + token + )); + } +} + +// File contracts/ln/base/LnOppositeBridgeTarget.sol +// License-Identifier: MIT + + +contract LnOppositeBridgeTarget is LnBridgeHelper { + uint256 constant public MIN_REFUND_TIMESTAMP = 30 * 60; + + // if slasher == address(0), this FillTransfer is relayed by lnProvider + // otherwise, this FillTransfer is slashed by slasher + // if there is no slash transfer before, then it's latestSlashTransferId is assigned by INIT_SLASH_TRANSFER_ID, a special flag + struct SlashInfo { + address provider; + address sourceToken; + address slasher; + } + + // transferId => latest slash transfer Id + mapping(bytes32 => bytes32) public fillTransfers; + // transferId => Slash info + mapping(bytes32 => SlashInfo) public slashInfos; + + event TransferFilled(bytes32 transferId, address slasher); + + // if slasher is nonzero, then it's a slash fill transfer + function _checkPreviousAndFillTransfer( + bytes32 transferId, + bytes32 previousTransferId + ) internal { + // the first fill transfer, we fill the INIT_SLASH_TRANSFER_ID as the latest slash transferId + if (previousTransferId == bytes32(0)) { + fillTransfers[transferId] = INIT_SLASH_TRANSFER_ID; + } else { + // Find the previous slash fill, it is a slash fill if the slasher is not zero address. + bytes32 previousLatestSlashTransferId = fillTransfers[previousTransferId]; + require(previousLatestSlashTransferId != bytes32(0), "previous fill not exist"); + + SlashInfo memory previousSlashInfo = slashInfos[previousTransferId]; + // we use latestSlashTransferId to store the latest slash transferId + // if previous.slasher != 0, then previous is slashed + // if previous.slasher == 0, then previous is not slashed + bytes32 latestSlashTransferId = previousSlashInfo.slasher != address(0) ? previousTransferId : previousLatestSlashTransferId; + + fillTransfers[transferId] = latestSlashTransferId; + } + } + + // fill transfer + // 1. if transfer is not slashed or relayed, LnProvider relay message to fill the transfer, and the transfer finished on target chain + // 2. if transfer is timeout and not processed, slasher(any account) can fill the transfer and request slash + // if it's filled by slasher, we store the address of the slasher + // expectedTransferId used to ensure the parameter is the same as on source chain + // some cases + // 1) If transferId is not exist on source chain, it'll be rejected by source chain when shashed. + // 2) If transferId exist on source chain. We have the same hash process on source and target chain, so the previousTransferId is trusted. + // 2.1) If transferId is the first transfer Id of this provider, then previousTransferId is zero and the latestSlashTransferId is INIT_SLASH_TRANSFER_ID + // 2.2) If transferId is not the first transfer, then it's latestSlashTransferId has the next two scenarios + // * the previousTransfer is a slash transfer, then latestSlashTransferId is previousTransferId + // * the previousTransfer is a normal relayed transfer, then latestSlashTransferId is previousTransfer's latestSlashTransferId + // I. transferId is trusted => previousTransferId is trusted => previousTransfer.previousTransferId is trusted => ... => firstTransfer is trusted + // II. transferId is trusted => previousTransferId is trusted => latestSlashTransferId is trusted if previousTransfer is a slash transfer + // III. Both I and II => latestSlashTransferId is trusted if previousTransfer is normal relayed tranfer + function _fillTransfer( + TransferParameter calldata params, + bytes32 expectedTransferId + ) internal { + bytes32 transferId = keccak256(abi.encodePacked( + params.previousTransferId, + params.provider, + params.sourceToken, + params.targetToken, + params.receiver, + params.timestamp, + params.amount)); + require(expectedTransferId == transferId, "check expected transferId failed"); + // Make sure this transfer was never filled before + require(fillTransfers[transferId] == bytes32(0), "fill exist"); + + _checkPreviousAndFillTransfer(transferId, params.previousTransferId); + + if (params.targetToken == address(0)) { + require(msg.value >= params.amount, "invalid amount"); + payable(params.receiver).transfer(params.amount); + } else { + _safeTransferFrom(params.targetToken, msg.sender, params.receiver, uint256(params.amount)); + } + } + + function transferAndReleaseMargin( + TransferParameter calldata params, + bytes32 expectedTransferId + ) payable external { + // normal relay message, fill slasher as zero + require(params.provider == msg.sender, "invalid provider"); + _fillTransfer(params, expectedTransferId); + + emit TransferFilled(expectedTransferId, address(0)); + } + + // The condition for slash is that the transfer has timed out + // Meanwhile we need to request a slash transaction to the source chain to withdraw the LnProvider's margin + // On the source chain, we need to verify all the transfers before has been relayed or slashed. + // So we needs to carry the the previous shash transferId to ensure that the slash is continuous. + function _slashAndRemoteRefund( + TransferParameter calldata params, + bytes32 expectedTransferId + ) internal returns(bytes memory message) { + require(block.timestamp > params.timestamp + MIN_REFUND_TIMESTAMP, "slash time not expired"); + _fillTransfer(params, expectedTransferId); + + // slasher = msg.sender + slashInfos[expectedTransferId] = SlashInfo(params.provider, params.sourceToken, msg.sender); + + // Do not slash `transferId` in source chain unless `latestSlashTransferId` has been slashed + message = _encodeSlashCall( + fillTransfers[expectedTransferId], + expectedTransferId, + params.provider, + params.sourceToken, + msg.sender + ); + emit TransferFilled(expectedTransferId, msg.sender); + } + + // we use this to verify that the transfer has been slashed by user and it can resend the slash request + function _retrySlashAndRemoteRefund(bytes32 transferId) internal view returns(bytes memory message) { + bytes32 latestSlashTransferId = fillTransfers[transferId]; + // transfer must be filled + require(latestSlashTransferId != bytes32(0), "invalid transfer id"); + // transfer must be slashed + SlashInfo memory slashInfo = slashInfos[transferId]; + require(slashInfo.slasher != address(0), "slasher not exist"); + message = _encodeSlashCall( + latestSlashTransferId, + transferId, + slashInfo.provider, + slashInfo.sourceToken, + slashInfo.slasher + ); + } + + function _encodeSlashCall( + bytes32 latestSlashTransferId, + bytes32 transferId, + address provider, + address sourceToken, + address slasher + ) internal pure returns(bytes memory) { + return abi.encodeWithSelector( + ILnOppositeBridgeSource.slash.selector, + latestSlashTransferId, + transferId, + provider, + sourceToken, + slasher + ); } - /** - * @dev Triggers stopped state. - * - * Requirements: - * - * - The contract must not be paused. - */ - function _pause() internal virtual whenNotPaused { - _paused = true; - emit Paused(_msgSender()); - } + function _requestWithdrawMargin( + bytes32 lastTransferId, + address sourceToken, + uint112 amount + ) internal view returns(bytes memory message) { + bytes32 latestSlashTransferId = fillTransfers[lastTransferId]; + require(latestSlashTransferId != bytes32(0), "invalid last transfer"); - /** - * @dev Returns to normal state. - * - * Requirements: - * - * - The contract must be paused. - */ - function _unpause() internal virtual whenPaused { - _paused = false; - emit Unpaused(_msgSender()); + return abi.encodeWithSelector( + ILnOppositeBridgeSource.withdrawMargin.selector, + latestSlashTransferId, + lastTransferId, + msg.sender, + sourceToken, + amount + ); } } @@ -267,6 +471,31 @@ interface IAccessControlEnumerable is IAccessControl { function getRoleMemberCount(bytes32 role) external view returns (uint256); } +// File @zeppelin-solidity/contracts/utils/Context.sol@v4.7.3 +// License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (utils/Context.sol) + + +/** + * @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; + } +} + // File @zeppelin-solidity/contracts/utils/Strings.sol@v4.7.3 // License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (utils/Strings.sol) @@ -1074,374 +1303,162 @@ abstract contract AccessControlEnumerable is IAccessControlEnumerable, AccessCon } } -// File contracts/ln/base/LnAccessController.sol -// License-Identifier: MIT - - -/// @title LnAccessController -/// @notice LnAccessController is a contract to control the access permission -/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract -contract LnAccessController is AccessControlEnumerable, Pausable { - bytes32 public constant DAO_ADMIN_ROLE = keccak256("DAO_ADMIN_ROLE"); - bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); - - modifier onlyDao() { - require(hasRole(DAO_ADMIN_ROLE, msg.sender), "lpBridge:Bad dao role"); - _; - } - - modifier onlyOperator() { - require(hasRole(OPERATOR_ROLE, msg.sender), "lpBridge:Bad operator role"); - _; - } - - function _initialize(address dao) internal { - _setRoleAdmin(OPERATOR_ROLE, DAO_ADMIN_ROLE); - _setRoleAdmin(DAO_ADMIN_ROLE, DAO_ADMIN_ROLE); - _setupRole(DAO_ADMIN_ROLE, dao); - _setupRole(OPERATOR_ROLE, msg.sender); - } - - function unpause() external onlyOperator { - _unpause(); - } - - function pause() external onlyOperator { - _pause(); - } -} - -// File @zeppelin-solidity/contracts/token/ERC20/IERC20.sol@v4.7.3 +// File @zeppelin-solidity/contracts/security/Pausable.sol@v4.7.3 // License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) +// OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) /** - * @dev Interface of the ERC20 standard as defined in the EIP. + * @dev Contract module which allows children to implement an emergency stop + * mechanism that can be triggered by an authorized account. + * + * This module is used through inheritance. It will make available the + * modifiers `whenNotPaused` and `whenPaused`, which can be applied to + * the functions of your contract. Note that they will not be pausable by + * simply including this module, only once the modifiers are put in place. */ -interface IERC20 { +abstract contract Pausable is Context { /** - * @dev Emitted when `value` tokens are moved from one account (`from`) to - * another (`to`). - * - * Note that `value` may be zero. + * @dev Emitted when the pause is triggered by `account`. */ - event Transfer(address indexed from, address indexed to, uint256 value); + event Paused(address account); /** - * @dev Emitted when the allowance of a `spender` for an `owner` is set by - * a call to {approve}. `value` is the new allowance. + * @dev Emitted when the pause is lifted by `account`. */ - event Approval(address indexed owner, address indexed spender, uint256 value); + event Unpaused(address account); - /** - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); + bool private _paused; /** - * @dev Returns the amount of tokens owned by `account`. + * @dev Initializes the contract in unpaused state. */ - function balanceOf(address account) external view returns (uint256); + constructor() { + _paused = false; + } /** - * @dev Moves `amount` tokens from the caller's account to `to`. - * - * Returns a boolean value indicating whether the operation succeeded. + * @dev Modifier to make a function callable only when the contract is not paused. * - * Emits a {Transfer} event. - */ - function transfer(address to, uint256 amount) 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. + * Requirements: * - * This value changes when {approve} or {transferFrom} are called. + * - The contract must not be paused. */ - function allowance(address owner, address spender) external view returns (uint256); + modifier whenNotPaused() { + _requireNotPaused(); + _; + } /** - * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. + * @dev Modifier to make a function callable only when the contract is paused. * - * 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 + * Requirements: * - * Emits an {Approval} event. + * - The contract must be paused. */ - function approve(address spender, uint256 amount) external returns (bool); + modifier whenPaused() { + _requirePaused(); + _; + } /** - * @dev Moves `amount` tokens from `from` to `to` using the - * allowance mechanism. `amount` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. + * @dev Returns true if the contract is paused, and false otherwise. */ - function transferFrom( - address from, - address to, - uint256 amount - ) external returns (bool); -} - -// File contracts/ln/base/LnBridgeHelper.sol -// License-Identifier: MIT - -contract LnBridgeHelper { - bytes32 constant public INIT_SLASH_TRANSFER_ID = bytes32(uint256(1)); - - struct TransferParameter { - bytes32 previousTransferId; - address provider; - address sourceToken; - address targetToken; - uint112 amount; - uint64 timestamp; - address receiver; - } - - function _safeTransfer( - address token, - address receiver, - uint256 amount - ) internal { - (bool success, bytes memory data) = token.call(abi.encodeWithSelector( - IERC20.transfer.selector, - receiver, - amount - )); - require(success && (data.length == 0 || abi.decode(data, (bool))), "lnBridgeHelper:transfer token failed"); - } - - function _safeTransferFrom( - address token, - address sender, - address receiver, - uint256 amount - ) internal { - (bool success, bytes memory data) = token.call(abi.encodeWithSelector( - IERC20.transferFrom.selector, - sender, - receiver, - amount - )); - require(success && (data.length == 0 || abi.decode(data, (bool))), "lnBridgeHelper:transferFrom token failed"); + function paused() public view virtual returns (bool) { + return _paused; } - function getProviderKey(address provider, address token) pure public returns(bytes32) { - return keccak256(abi.encodePacked( - provider, - token - )); + /** + * @dev Throws if the contract is paused. + */ + function _requireNotPaused() internal view virtual { + require(!paused(), "Pausable: paused"); } -} - -// File contracts/ln/interface/ILnOppositeBridgeSource.sol -// License-Identifier: MIT + /** + * @dev Throws if the contract is not paused. + */ + function _requirePaused() internal view virtual { + require(paused(), "Pausable: not paused"); + } -interface ILnOppositeBridgeSource { - function slash( - bytes32 lastRefundTransferId, - bytes32 transferId, - address provider, - address sourceToken, - address slasher - ) external; + /** + * @dev Triggers stopped state. + * + * Requirements: + * + * - The contract must not be paused. + */ + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(_msgSender()); + } - function withdrawMargin( - bytes32 lastRefundTransferId, - bytes32 lastTransferId, - address provider, - address sourceToken, - uint112 amount - ) external; + /** + * @dev Returns to normal state. + * + * Requirements: + * + * - The contract must be paused. + */ + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(_msgSender()); + } } -// File contracts/ln/base/LnOppositeBridgeTarget.sol +// File contracts/ln/base/LnAccessController.sol // License-Identifier: MIT -contract LnOppositeBridgeTarget is LnBridgeHelper { - uint256 constant public MIN_REFUND_TIMESTAMP = 30 * 60; +/// @title LnAccessController +/// @notice LnAccessController is a contract to control the access permission +/// @dev See https://github.com/helix-bridge/contracts/tree/master/helix-contract +contract LnAccessController is AccessControlEnumerable, Pausable { + bytes32 public constant DAO_ADMIN_ROLE = keccak256("DAO_ADMIN_ROLE"); + bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE"); - // if slasher == address(0), this FillTransfer is relayed by lnProvider - // otherwise, this FillTransfer is slashed by slasher - // if there is no slash transfer before, then it's latestSlashTransferId is assigned by INIT_SLASH_TRANSFER_ID, a special flag - struct SlashInfo { - address provider; - address sourceToken; - address slasher; + modifier onlyDao() { + require(hasRole(DAO_ADMIN_ROLE, msg.sender), "lpBridge:Bad dao role"); + _; } - // transferId => latest slash transfer Id - mapping(bytes32 => bytes32) public fillTransfers; - // transferId => Slash info - mapping(bytes32 => SlashInfo) public slashInfos; - - event TransferFilled(bytes32 transferId, address slasher); - - // if slasher is nonzero, then it's a slash fill transfer - function _checkPreviousAndFillTransfer( - bytes32 transferId, - bytes32 previousTransferId - ) internal { - // the first fill transfer, we fill the INIT_SLASH_TRANSFER_ID as the latest slash transferId - if (previousTransferId == bytes32(0)) { - fillTransfers[transferId] = INIT_SLASH_TRANSFER_ID; - } else { - // Find the previous slash fill, it is a slash fill if the slasher is not zero address. - bytes32 previousLatestSlashTransferId = fillTransfers[previousTransferId]; - require(previousLatestSlashTransferId != bytes32(0), "previous fill not exist"); - - SlashInfo memory previousSlashInfo = slashInfos[previousTransferId]; - // we use latestSlashTransferId to store the latest slash transferId - // if previous.slasher != 0, then previous is slashed - // if previous.slasher == 0, then previous is not slashed - bytes32 latestSlashTransferId = previousSlashInfo.slasher != address(0) ? previousTransferId : previousLatestSlashTransferId; - - fillTransfers[transferId] = latestSlashTransferId; - } + modifier onlyOperator() { + require(hasRole(OPERATOR_ROLE, msg.sender), "lpBridge:Bad operator role"); + _; } - // fill transfer - // 1. if transfer is not slashed or relayed, LnProvider relay message to fill the transfer, and the transfer finished on target chain - // 2. if transfer is timeout and not processed, slasher(any account) can fill the transfer and request slash - // if it's filled by slasher, we store the address of the slasher - // expectedTransferId used to ensure the parameter is the same as on source chain - // some cases - // 1) If transferId is not exist on source chain, it'll be rejected by source chain when shashed. - // 2) If transferId exist on source chain. We have the same hash process on source and target chain, so the previousTransferId is trusted. - // 2.1) If transferId is the first transfer Id of this provider, then previousTransferId is zero and the latestSlashTransferId is INIT_SLASH_TRANSFER_ID - // 2.2) If transferId is not the first transfer, then it's latestSlashTransferId has the next two scenarios - // * the previousTransfer is a slash transfer, then latestSlashTransferId is previousTransferId - // * the previousTransfer is a normal relayed transfer, then latestSlashTransferId is previousTransfer's latestSlashTransferId - // I. transferId is trusted => previousTransferId is trusted => previousTransfer.previousTransferId is trusted => ... => firstTransfer is trusted - // II. transferId is trusted => previousTransferId is trusted => latestSlashTransferId is trusted if previousTransfer is a slash transfer - // III. Both I and II => latestSlashTransferId is trusted if previousTransfer is normal relayed tranfer - function _fillTransfer( - TransferParameter calldata params, - bytes32 expectedTransferId - ) internal { - bytes32 transferId = keccak256(abi.encodePacked( - params.previousTransferId, - params.provider, - params.sourceToken, - params.targetToken, - params.receiver, - params.timestamp, - params.amount)); - require(expectedTransferId == transferId, "check expected transferId failed"); - // Make sure this transfer was never filled before - require(fillTransfers[transferId] == bytes32(0), "fill exist"); - - _checkPreviousAndFillTransfer(transferId, params.previousTransferId); - - if (params.targetToken == address(0)) { - require(msg.value >= params.amount, "invalid amount"); - payable(params.receiver).transfer(params.amount); - } else { - _safeTransferFrom(params.targetToken, msg.sender, params.receiver, uint256(params.amount)); - } + function _initialize(address dao) internal { + _setRoleAdmin(OPERATOR_ROLE, DAO_ADMIN_ROLE); + _setRoleAdmin(DAO_ADMIN_ROLE, DAO_ADMIN_ROLE); + _setupRole(DAO_ADMIN_ROLE, dao); + _setupRole(OPERATOR_ROLE, msg.sender); } - function transferAndReleaseMargin( - TransferParameter calldata params, - bytes32 expectedTransferId - ) payable external { - // normal relay message, fill slasher as zero - require(params.provider == msg.sender, "invalid provider"); - _fillTransfer(params, expectedTransferId); - - emit TransferFilled(expectedTransferId, address(0)); + function unpause() external onlyOperator { + _unpause(); } - // The condition for slash is that the transfer has timed out - // Meanwhile we need to request a slash transaction to the source chain to withdraw the LnProvider's margin - // On the source chain, we need to verify all the transfers before has been relayed or slashed. - // So we needs to carry the the previous shash transferId to ensure that the slash is continuous. - function _slashAndRemoteRefund( - TransferParameter calldata params, - bytes32 expectedTransferId - ) internal returns(bytes memory message) { - require(block.timestamp > params.timestamp + MIN_REFUND_TIMESTAMP, "slash time not expired"); - _fillTransfer(params, expectedTransferId); - - // slasher = msg.sender - slashInfos[expectedTransferId] = SlashInfo(params.provider, params.sourceToken, msg.sender); - - // Do not slash `transferId` in source chain unless `latestSlashTransferId` has been slashed - message = _encodeSlashCall( - fillTransfers[expectedTransferId], - expectedTransferId, - params.provider, - params.sourceToken, - msg.sender - ); - emit TransferFilled(expectedTransferId, msg.sender); + function pause() external onlyOperator { + _pause(); } +} - // we use this to verify that the transfer has been slashed by user and it can resend the slash request - function _retrySlashAndRemoteRefund(bytes32 transferId) internal view returns(bytes memory message) { - bytes32 latestSlashTransferId = fillTransfers[transferId]; - // transfer must be filled - require(latestSlashTransferId != bytes32(0), "invalid transfer id"); - // transfer must be slashed - SlashInfo memory slashInfo = slashInfos[transferId]; - require(slashInfo.slasher != address(0), "slasher not exist"); - message = _encodeSlashCall( - latestSlashTransferId, - transferId, - slashInfo.provider, - slashInfo.sourceToken, - slashInfo.slasher - ); - } +// File @arbitrum/nitro-contracts/src/bridge/IDelayedMessageProvider.sol@v1.0.1 +// Copyright 2021-2022, Offchain Labs, Inc. +// For license information, see https://github.com/nitro/blob/master/LICENSE +// License-Identifier: BUSL-1.1 - function _encodeSlashCall( - bytes32 latestSlashTransferId, - bytes32 transferId, - address provider, - address sourceToken, - address slasher - ) internal pure returns(bytes memory) { - return abi.encodeWithSelector( - ILnOppositeBridgeSource.slash.selector, - latestSlashTransferId, - transferId, - provider, - sourceToken, - slasher - ); - } +// solhint-disable-next-line compiler-version +pragma solidity >=0.6.9 <0.9.0; - function _requestWithdrawMargin( - bytes32 lastTransferId, - address sourceToken, - uint112 amount - ) internal view returns(bytes memory message) { - bytes32 latestSlashTransferId = fillTransfers[lastTransferId]; - require(latestSlashTransferId != bytes32(0), "invalid last transfer"); +interface IDelayedMessageProvider { + /// @dev event emitted when a inbox message is added to the Bridge's delayed accumulator + event InboxMessageDelivered(uint256 indexed messageNum, bytes data); - return abi.encodeWithSelector( - ILnOppositeBridgeSource.withdrawMargin.selector, - latestSlashTransferId, - lastTransferId, - msg.sender, - sourceToken, - amount - ); - } + /// @dev event emitted when a inbox message is added to the Bridge's delayed accumulator + /// same as InboxMessageDelivered but the batch data is available in tx.input + event InboxMessageDeliveredFromOrigin(uint256 indexed messageNum); } // File @arbitrum/nitro-contracts/src/bridge/IOwnable.sol@v1.0.1 @@ -1571,23 +1588,6 @@ interface IBridge { function initialize(IOwnable rollup_) external; } -// File @arbitrum/nitro-contracts/src/bridge/IDelayedMessageProvider.sol@v1.0.1 -// Copyright 2021-2022, Offchain Labs, Inc. -// For license information, see https://github.com/nitro/blob/master/LICENSE -// License-Identifier: BUSL-1.1 - -// solhint-disable-next-line compiler-version -pragma solidity >=0.6.9 <0.9.0; - -interface IDelayedMessageProvider { - /// @dev event emitted when a inbox message is added to the Bridge's delayed accumulator - event InboxMessageDelivered(uint256 indexed messageNum, bytes data); - - /// @dev event emitted when a inbox message is added to the Bridge's delayed accumulator - /// same as InboxMessageDelivered but the batch data is available in tx.input - event InboxMessageDeliveredFromOrigin(uint256 indexed messageNum); -} - // File @arbitrum/nitro-contracts/src/libraries/IGasRefunder.sol@v1.0.1 // Copyright 2021-2022, Offchain Labs, Inc. // For license information, see https://github.com/nitro/blob/master/LICENSE diff --git a/helix-contract/test/3_test_ln.js b/helix-contract/test/3_test_ln.js index 9fc6b9b3..96c3a298 100644 --- a/helix-contract/test/3_test_ln.js +++ b/helix-contract/test/3_test_ln.js @@ -487,6 +487,34 @@ describe("arb<>eth lnv2 bridge tests", () => { transferId05 )).to.revertedWith("fill exist"); + // test paused + await lnBridgeOnL2.connect(relayer).providerPause(arbToken.address); + await expect(lnBridgeOnL2.connect(other).transferAndLockMargin( + [ + relayer.address, + arbToken.address, + transferId05, + margin, + expectedFee + ], + transferAmount01, // amount + other.address, // receiver + { value: transferAmount01 + expectedFee } + )).to.be.revertedWith("provider paused"); + await lnBridgeOnL2.connect(relayer).providerUnpause(arbToken.address); + await expect(lnBridgeOnL2.connect(other).transferAndLockMargin( + [ + relayer.address, + arbToken.address, + transferId05, + margin, + expectedFee + ], + transferAmount01, // amount + other.address, // receiver + { value: transferAmount01 + expectedFee } + )).to.be.revertedWith("margin updated"); + console.log("ln bridge test finished"); });