diff --git a/basic/80-crossChainTransfer/celerBridge/.env.example b/basic/80-crossChainTransfer/celerBridge/.env.example new file mode 100644 index 000000000..8490799ed --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/.env.example @@ -0,0 +1,8 @@ +PRIVATE_KEY=xxxx +PRIVATE_KEY1=yyyy +PRIVATE_KEY2=yyyy +INFURA_ID=yyyy +PROJECT_ID=yyyy +TARGET_ACCOUNT=yyyy +API_KEY=yyyy +EHTERSCAN_KEY=yyyy diff --git a/basic/80-crossChainTransfer/celerBridge/.gitignore b/basic/80-crossChainTransfer/celerBridge/.gitignore new file mode 100644 index 000000000..d0f33a7c1 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/.gitignore @@ -0,0 +1,11 @@ +node_modules +.env +coverage +coverage.json +typechain + +#Hardhat files +cache +artifacts + +**/deployment.json \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/README-cn.md b/basic/80-crossChainTransfer/celerBridge/README-cn.md new file mode 100644 index 000000000..542b02f05 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/README-cn.md @@ -0,0 +1,71 @@ +# Celer Bridge 跨链 +中文 / [English](./README.md) + +## 概览 +cBridge 引入了最佳的跨链代币桥接体验,为用户提供了深度流动性,为 cBridge 节点运营商和不想运营 cBridge 节点的流动性提供者提供了高效且易于使用的流动性管理,以及新的令人兴奋的面向开发者的功能,如用于跨链 DEX 和 NFT 等场景的通用消息桥接。 + +cBridge 有两种 bridge 方式,一种是 Pool-Based ,一种是 Canonical Mapping。 +Pool-Based 就是在 A 链和 B 链之间各自锁定相同的 token,比如 USDT。当用户需要从 A 链跨到 B 链的时候,先会把 USDT 转入 A 链这边的 Vault 中,然后在 B 链这边把 USDT transfer 给用户。这个情况下,就需要在 A 链和 B 链上各自建立 pool 来完成这个操作,当 pool 中的资金不足时,就会出现无法 bridge 的情况。这种模式被称为 lock/unlock。 +Canonical Mapping 对应的就是 lock/mint。用户 bridge 的时候,会把 USDT lock 到 A 链这边的 vault,然后在 B 链这边 mint 出对应的资产给用户。反过来,当用户想把 B 链上的 USDT bridge 会 A 链的时候,会把 B 链上的 USDT burn 掉,然后在 A 链这边把 USDT 从 vault 再 transfer 给用户。 + +本测试将以 OP mainnet 和 Polygon mainnet 进行测试 + +## 测试环境要求 +node 版本需要为 v18.17.0, 可以使用 nvm 切换当前 node 版本 + +## 准备测试环境 +- 安装依赖 +``` +npm install +``` + +- 配置环境 +``` +cp .env.example .env +## 之后在 .env 中配置具体的私钥 +``` + +## 执行跨链 +- 以流动性池方式进行跨链 +``` +npx hardhat run scripts/1-poolBasedTransfer.js --network optim +``` + +- 检查跨链结果 +``` +npx hardhat run scripts/2-queryPoolBasedTrasnferStatus.js --network optim +``` + +- 以映射方式进行跨链 +``` +npx hardhat run scripts/3.1-canonicalTokenTransfer.js --network optim +``` + +- 检查跨链结果 +大概需要 15 分钟左右才能确定最终跨链状态,可等待 15 分钟后再来查询结果 +``` +npx hardhat run scripts/4.1-queryCanonicalTrasnferStatus.js --network optim +``` + +## 跨链 Refund +当跨链失败的时候,用户可以 refund 他的资产,以下测试如何进行 refund + +- 以映射方式进行跨链 +``` +## 为测试需要,本次跨链将会失败,OP tonken 会被 unlock,为后续的 refund 做准备 +npx hardhat run scripts/3.2-canonicalTokenTransfer_ForRefund.js --network optim +``` + +- 检查跨链结果 +大概需要 15 分钟左右才能确定最终跨链状态,可等待 15 分钟后再来查询结果 +``` +npx hardhat run scripts/4.2-queryCanonicalTrasnferStatus_ForRefund.js --network optim +``` + +- Refund +``` +npx hardhat run scripts/5-canonicalTrasnferRefund.js --network optim +``` + +## 参考文档 +- 官方 doc: https://cbridge-docs.celer.network/ \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/README.md b/basic/80-crossChainTransfer/celerBridge/README.md new file mode 100644 index 000000000..35b44dfd6 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/README.md @@ -0,0 +1,72 @@ +# Celer Bridge +[中文](./README-cn.md) / English + +## Overview +cBridge introduces the best cross-chain token bridging experience, providing users with deep liquidity, efficient and easy-to-use liquidity management for cBridge node operators and liquidity providers who do not want to operate cBridge nodes. It also offers exciting developer-oriented features such as a universal message bridging for scenarios like cross-chain DEX and NFT. + +cBridge has two bridge modes, one is Pool-Based, and the other is Canonical Mapping. +Pool-Based involves locking the same token on both Chain A and Chain B, taking USDT as an example. When a user needs to cross from Chain A to Chain B, they first deposit USDT into the Vault on Chain A, and then transfer the USDT to the user on Chain B. In this case, pools need to be established on Chain A and Chain B respectively to complete this operation. When the funds in the pool are insufficient, it may result in an inability to bridge. This mode is known as lock/unlock. +Canonical Mapping corresponds to lock/mint. When a user bridges, they lock USDT into the vault on Chain A, and then mint the corresponding asset to the user on Chain B. Conversely, when a user wants to bridge USDT from Chain B back to Chain A, they burn the USDT on Chain B, and then transfer the USDT from the vault to the user on Chain A. + +This test will be conducted on the OP mainnet and Polygon mainnet. + +## Test Environment Requirements +The node version needs to be v18.17.0, you can use nvm to switch to the current node version. + + +## Preparing the Testing Environment +- Install dependencies +``` +npm install +``` + +- Configure the environment +``` +cp .env.example .env +## Then configure the specific private key in .env +``` + +## Executing Cross-Chain Operations +- Cross-chain operation in Pool-Based mode +``` +npx hardhat run scripts/1-poolBasedTransfer.js --network optim +``` + +- Check the cross-chain results +``` +npx hardhat run scripts/2-queryPoolBasedTrasnferStatus.js --network optim +``` + +- Cross-chain operation in the mapping mode +``` +npx hardhat run scripts/3.1-canonicalTokenTransfer.js --network optim +``` + +- Check the cross-chain results +It takes approximately 15 minutes to confirm the final cross-chain status. You can wait for 15 minutes before querying the results. +``` +npx hardhat run scripts/4.1-queryCanonicalTrasnferStatus.js --network optim +``` + +## Cross-Chain Refund +When a cross-chain operation fails, users can refund their assets. The following tests demonstrate how to initiate a refund. + +- Cross-chain operation using the mapping method +``` +## For testing purposes, this cross-chain operation will fail, and the OP token will be unlocked, preparing for the subsequent refund. +npx hardhat run scripts/3.2-canonicalTokenTransfer_ForRefund.js --network optim +``` + +- Check the cross-chain results +It takes approximately 15 minutes to confirm the final cross-chain status. You can wait for 15 minutes before querying the results. +``` +npx hardhat run scripts/4.2-queryCanonicalTrasnferStatus_ForRefund.js --network optim +``` + +- Refund +``` +npx hardhat run scripts/5-canonicalTrasnferRefund.js --network optim +``` + +## Reference Documentation +- Official doc: https://cbridge-docs.celer.network/ \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/Bridge.sol b/basic/80-crossChainTransfer/celerBridge/contracts/Bridge.sol new file mode 100644 index 000000000..c0bea5b62 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/Bridge.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "./libraries/PbBridge.sol"; +import "./Pool.sol"; + +/** + * @title The liquidity-pool based bridge. + */ +contract Bridge is Pool { + using SafeERC20 for IERC20; + + // liquidity events + event Send( + bytes32 transferId, + address sender, + address receiver, + address token, + uint256 amount, + uint64 dstChainId, + uint64 nonce, + uint32 maxSlippage + ); + event Relay( + bytes32 transferId, + address sender, + address receiver, + address token, + uint256 amount, + uint64 srcChainId, + bytes32 srcTransferId + ); + // gov events + event MinSendUpdated(address token, uint256 amount); + event MaxSendUpdated(address token, uint256 amount); + + mapping(bytes32 => bool) public transfers; + mapping(address => uint256) public minSend; // send _amount must > minSend + mapping(address => uint256) public maxSend; + + // min allowed max slippage uint32 value is slippage * 1M, eg. 0.5% -> 5000 + uint32 public minimalMaxSlippage; + + /** + * @notice Send a cross-chain transfer via the liquidity pool-based bridge. + * NOTE: This function DOES NOT SUPPORT fee-on-transfer / rebasing tokens. + * @param _receiver The address of the receiver. + * @param _token The address of the token. + * @param _amount The amount of the transfer. + * @param _dstChainId The destination chain ID. + * @param _nonce A number input to guarantee uniqueness of transferId. Can be timestamp in practice. + * @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%. + * Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least (100% - max slippage percentage) * amount or the + * transfer can be refunded. + */ + function send( + address _receiver, + address _token, + uint256 _amount, + uint64 _dstChainId, + uint64 _nonce, + uint32 _maxSlippage // slippage * 1M, eg. 0.5% -> 5000 + ) external nonReentrant whenNotPaused { + bytes32 transferId = _send(_receiver, _token, _amount, _dstChainId, _nonce, _maxSlippage); + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + emit Send(transferId, msg.sender, _receiver, _token, _amount, _dstChainId, _nonce, _maxSlippage); + } + + /** + * @notice Send a cross-chain transfer via the liquidity pool-based bridge using the native token. + * @param _receiver The address of the receiver. + * @param _amount The amount of the transfer. + * @param _dstChainId The destination chain ID. + * @param _nonce A unique number. Can be timestamp in practice. + * @param _maxSlippage The max slippage accepted, given as percentage in point (pip). Eg. 5000 means 0.5%. + * Must be greater than minimalMaxSlippage. Receiver is guaranteed to receive at least (100% - max slippage percentage) * amount or the + * transfer can be refunded. + */ + function sendNative( + address _receiver, + uint256 _amount, + uint64 _dstChainId, + uint64 _nonce, + uint32 _maxSlippage + ) external payable nonReentrant whenNotPaused { + require(msg.value == _amount, "Amount mismatch"); + require(nativeWrap != address(0), "Native wrap not set"); + bytes32 transferId = _send(_receiver, nativeWrap, _amount, _dstChainId, _nonce, _maxSlippage); + IWETH(nativeWrap).deposit{value: _amount}(); + emit Send(transferId, msg.sender, _receiver, nativeWrap, _amount, _dstChainId, _nonce, _maxSlippage); + } + + function _send( + address _receiver, + address _token, + uint256 _amount, + uint64 _dstChainId, + uint64 _nonce, + uint32 _maxSlippage + ) private returns (bytes32) { + require(_amount > minSend[_token], "amount too small"); + require(maxSend[_token] == 0 || _amount <= maxSend[_token], "amount too large"); + require(_maxSlippage > minimalMaxSlippage, "max slippage too small"); + bytes32 transferId = keccak256( + // uint64(block.chainid) for consistency as entire system uses uint64 for chain id + // len = 20 + 20 + 20 + 32 + 8 + 8 + 8 = 116 + abi.encodePacked(msg.sender, _receiver, _token, _amount, _dstChainId, _nonce, uint64(block.chainid)) + ); + require(transfers[transferId] == false, "transfer exists"); + transfers[transferId] = true; + return transferId; + } + + /** + * @notice Relay a cross-chain transfer sent from a liquidity pool-based bridge on another chain. + * @param _relayRequest The serialized Relay protobuf. + * @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by + * +2/3 of the bridge's current signing power to be delivered. + * @param _signers The sorted list of signers. + * @param _powers The signing powers of the signers. + */ + function relay( + bytes calldata _relayRequest, + bytes[] calldata _sigs, + address[] calldata _signers, + uint256[] calldata _powers + ) external whenNotPaused { + bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "Relay")); + verifySigs(abi.encodePacked(domain, _relayRequest), _sigs, _signers, _powers); + PbBridge.Relay memory request = PbBridge.decRelay(_relayRequest); + // len = 20 + 20 + 20 + 32 + 8 + 8 + 32 = 140 + bytes32 transferId = keccak256( + abi.encodePacked( + request.sender, + request.receiver, + request.token, + request.amount, + request.srcChainId, + request.dstChainId, + request.srcTransferId + ) + ); + require(transfers[transferId] == false, "transfer exists"); + transfers[transferId] = true; + _updateVolume(request.token, request.amount); + uint256 delayThreshold = delayThresholds[request.token]; + if (delayThreshold > 0 && request.amount > delayThreshold) { + _addDelayedTransfer(transferId, request.receiver, request.token, request.amount); + } else { + _sendToken(request.receiver, request.token, request.amount); + } + + emit Relay( + transferId, + request.sender, + request.receiver, + request.token, + request.amount, + request.srcChainId, + request.srcTransferId + ); + } + + function setMinSend(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor { + require(_tokens.length == _amounts.length, "length mismatch"); + for (uint256 i = 0; i < _tokens.length; i++) { + minSend[_tokens[i]] = _amounts[i]; + emit MinSendUpdated(_tokens[i], _amounts[i]); + } + } + + function setMaxSend(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor { + require(_tokens.length == _amounts.length, "length mismatch"); + for (uint256 i = 0; i < _tokens.length; i++) { + maxSend[_tokens[i]] = _amounts[i]; + emit MaxSendUpdated(_tokens[i], _amounts[i]); + } + } + + function setMinimalMaxSlippage(uint32 _minimalMaxSlippage) external onlyGovernor { + minimalMaxSlippage = _minimalMaxSlippage; + } + + // This is needed to receive ETH when calling `IWETH.withdraw` + receive() external payable {} +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/OriginalTokenVault.sol b/basic/80-crossChainTransfer/celerBridge/contracts/OriginalTokenVault.sol new file mode 100644 index 000000000..d10a793d0 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/OriginalTokenVault.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "./interfaces/ISigsVerifier.sol"; +import "./interfaces/IWETH.sol"; +import "./libraries/PbPegged.sol"; +import "./safeguard/Pauser.sol"; +import "./safeguard/VolumeControl.sol"; +import "./safeguard/DelayedTransfer.sol"; + +/** + * @title the vault to deposit and withdraw original tokens + * @dev Work together with PeggedTokenBridge contracts deployed at remote chains + */ +contract OriginalTokenVault is ReentrancyGuard, Pauser, VolumeControl, DelayedTransfer { + using SafeERC20 for IERC20; + + ISigsVerifier public immutable sigsVerifier; + + mapping(bytes32 => bool) public records; + + mapping(address => uint256) public minDeposit; + mapping(address => uint256) public maxDeposit; + + address public nativeWrap; + + event Deposited( + bytes32 depositId, + address depositor, + address token, + uint256 amount, + uint64 mintChainId, + address mintAccount + ); + event Withdrawn( + bytes32 withdrawId, + address receiver, + address token, + uint256 amount, + uint64 refChainId, + bytes32 refId, + address burnAccount + ); + event MinDepositUpdated(address token, uint256 amount); + event MaxDepositUpdated(address token, uint256 amount); + + constructor(ISigsVerifier _sigsVerifier) { + sigsVerifier = _sigsVerifier; + } + + /** + * @notice Lock original tokens to trigger cross-chain mint of pegged tokens at a remote chain's PeggedTokenBridge. + * NOTE: This function DOES NOT SUPPORT fee-on-transfer / rebasing tokens. + * @param _token The original token address. + * @param _amount The amount to deposit. + * @param _mintChainId The destination chain ID to mint tokens. + * @param _mintAccount The destination account to receive the minted pegged tokens. + * @param _nonce A number input to guarantee unique depositId. Can be timestamp in practice. + */ + function deposit( + address _token, + uint256 _amount, + uint64 _mintChainId, + address _mintAccount, + uint64 _nonce + ) external nonReentrant whenNotPaused { + bytes32 depId = _deposit(_token, _amount, _mintChainId, _mintAccount, _nonce); + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + emit Deposited(depId, msg.sender, _token, _amount, _mintChainId, _mintAccount); + } + + /** + * @notice Lock native token as original token to trigger cross-chain mint of pegged tokens at a remote chain's + * PeggedTokenBridge. + * @param _amount The amount to deposit. + * @param _mintChainId The destination chain ID to mint tokens. + * @param _mintAccount The destination account to receive the minted pegged tokens. + * @param _nonce A number input to guarantee unique depositId. Can be timestamp in practice. + */ + function depositNative( + uint256 _amount, + uint64 _mintChainId, + address _mintAccount, + uint64 _nonce + ) external payable nonReentrant whenNotPaused { + require(msg.value == _amount, "Amount mismatch"); + require(nativeWrap != address(0), "Native wrap not set"); + bytes32 depId = _deposit(nativeWrap, _amount, _mintChainId, _mintAccount, _nonce); + IWETH(nativeWrap).deposit{value: _amount}(); + emit Deposited(depId, msg.sender, nativeWrap, _amount, _mintChainId, _mintAccount); + } + + function _deposit( + address _token, + uint256 _amount, + uint64 _mintChainId, + address _mintAccount, + uint64 _nonce + ) private returns (bytes32) { + require(_amount > minDeposit[_token], "amount too small"); + require(maxDeposit[_token] == 0 || _amount <= maxDeposit[_token], "amount too large"); + bytes32 depId = keccak256( + // len = 20 + 20 + 32 + 8 + 20 + 8 + 8 = 116 + abi.encodePacked(msg.sender, _token, _amount, _mintChainId, _mintAccount, _nonce, uint64(block.chainid)) + ); + require(records[depId] == false, "record exists"); + records[depId] = true; + return depId; + } + + /** + * @notice Withdraw locked original tokens triggered by a burn at a remote chain's PeggedTokenBridge. + * @param _request The serialized Withdraw protobuf. + * @param _sigs The list of signatures sorted by signing addresses in ascending order. A relay must be signed-off by + * +2/3 of the bridge's current signing power to be delivered. + * @param _signers The sorted list of signers. + * @param _powers The signing powers of the signers. + */ + function withdraw( + bytes calldata _request, + bytes[] calldata _sigs, + address[] calldata _signers, + uint256[] calldata _powers + ) external whenNotPaused { + bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "Withdraw")); + sigsVerifier.verifySigs(abi.encodePacked(domain, _request), _sigs, _signers, _powers); + PbPegged.Withdraw memory request = PbPegged.decWithdraw(_request); + bytes32 wdId = keccak256( + // len = 20 + 20 + 32 + 20 + 8 + 32 = 132 + abi.encodePacked( + request.receiver, + request.token, + request.amount, + request.burnAccount, + request.refChainId, + request.refId + ) + ); + require(records[wdId] == false, "record exists"); + records[wdId] = true; + _updateVolume(request.token, request.amount); + uint256 delayThreshold = delayThresholds[request.token]; + if (delayThreshold > 0 && request.amount > delayThreshold) { + _addDelayedTransfer(wdId, request.receiver, request.token, request.amount); + } else { + _sendToken(request.receiver, request.token, request.amount); + } + emit Withdrawn( + wdId, + request.receiver, + request.token, + request.amount, + request.refChainId, + request.refId, + request.burnAccount + ); + } + + function executeDelayedTransfer(bytes32 id) external whenNotPaused { + delayedTransfer memory transfer = _executeDelayedTransfer(id); + _sendToken(transfer.receiver, transfer.token, transfer.amount); + } + + function setMinDeposit(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor { + require(_tokens.length == _amounts.length, "length mismatch"); + for (uint256 i = 0; i < _tokens.length; i++) { + minDeposit[_tokens[i]] = _amounts[i]; + emit MinDepositUpdated(_tokens[i], _amounts[i]); + } + } + + function setMaxDeposit(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor { + require(_tokens.length == _amounts.length, "length mismatch"); + for (uint256 i = 0; i < _tokens.length; i++) { + maxDeposit[_tokens[i]] = _amounts[i]; + emit MaxDepositUpdated(_tokens[i], _amounts[i]); + } + } + + function setWrap(address _weth) external onlyOwner { + nativeWrap = _weth; + } + + function _sendToken( + address _receiver, + address _token, + uint256 _amount + ) private { + if (_token == nativeWrap) { + // withdraw then transfer native to receiver + IWETH(nativeWrap).withdraw(_amount); + (bool sent, ) = _receiver.call{value: _amount, gas: 50000}(""); + require(sent, "failed to send native token"); + } else { + IERC20(_token).safeTransfer(_receiver, _amount); + } + } + + receive() external payable {} +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/Pool.sol b/basic/80-crossChainTransfer/celerBridge/contracts/Pool.sol new file mode 100644 index 000000000..74ecd67cb --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/Pool.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "./interfaces/IWETH.sol"; +import "./libraries/PbPool.sol"; +import "./safeguard/Pauser.sol"; +import "./safeguard/VolumeControl.sol"; +import "./safeguard/DelayedTransfer.sol"; +import "./Signers.sol"; + +/** + * @title Liquidity pool functions for {Bridge}. + */ +contract Pool is Signers, ReentrancyGuard, Pauser, VolumeControl, DelayedTransfer { + using SafeERC20 for IERC20; + + uint64 public addseq; // ensure unique LiquidityAdded event, start from 1 + mapping(address => uint256) public minAdd; // add _amount must > minAdd + + // map of successful withdraws, if true means already withdrew money or added to delayedTransfers + mapping(bytes32 => bool) public withdraws; + + // erc20 wrap of gas token of this chain, eg. WETH, when relay ie. pay out, + // if request.token equals this, will withdraw and send native token to receiver + // note we don't check whether it's zero address. when this isn't set, and request.token + // is all 0 address, guarantee fail + address public nativeWrap; + + // when transfer native token after wrap, use this gas used config. + uint256 public nativeTokenTransferGas = 50000; + + // liquidity events + event LiquidityAdded( + uint64 seqnum, + address provider, + address token, + uint256 amount // how many tokens were added + ); + event WithdrawDone( + bytes32 withdrawId, + uint64 seqnum, + address receiver, + address token, + uint256 amount, + bytes32 refid + ); + event MinAddUpdated(address token, uint256 amount); + + /** + * @notice Add liquidity to the pool-based bridge. + * NOTE: This function DOES NOT SUPPORT fee-on-transfer / rebasing tokens. + * @param _token The address of the token. + * @param _amount The amount to add. + */ + function addLiquidity(address _token, uint256 _amount) external nonReentrant whenNotPaused { + require(_amount > minAdd[_token], "amount too small"); + addseq += 1; + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + emit LiquidityAdded(addseq, msg.sender, _token, _amount); + } + + /** + * @notice Add native token liquidity to the pool-based bridge. + * @param _amount The amount to add. + */ + function addNativeLiquidity(uint256 _amount) external payable nonReentrant whenNotPaused { + require(msg.value == _amount, "Amount mismatch"); + require(nativeWrap != address(0), "Native wrap not set"); + require(_amount > minAdd[nativeWrap], "amount too small"); + addseq += 1; + IWETH(nativeWrap).deposit{value: _amount}(); + emit LiquidityAdded(addseq, msg.sender, nativeWrap, _amount); + } + + /** + * @notice Withdraw funds from the bridge pool. + * @param _wdmsg The serialized Withdraw protobuf. + * @param _sigs The list of signatures sorted by signing addresses in ascending order. A withdrawal must be + * signed-off by +2/3 of the bridge's current signing power to be delivered. + * @param _signers The sorted list of signers. + * @param _powers The signing powers of the signers. + */ + function withdraw( + bytes calldata _wdmsg, + bytes[] calldata _sigs, + address[] calldata _signers, + uint256[] calldata _powers + ) external whenNotPaused { + bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "WithdrawMsg")); + verifySigs(abi.encodePacked(domain, _wdmsg), _sigs, _signers, _powers); + // decode and check wdmsg + PbPool.WithdrawMsg memory wdmsg = PbPool.decWithdrawMsg(_wdmsg); + // len = 8 + 8 + 20 + 20 + 32 = 88 + bytes32 wdId = keccak256( + abi.encodePacked(wdmsg.chainid, wdmsg.seqnum, wdmsg.receiver, wdmsg.token, wdmsg.amount) + ); + require(withdraws[wdId] == false, "withdraw already succeeded"); + withdraws[wdId] = true; + _updateVolume(wdmsg.token, wdmsg.amount); + uint256 delayThreshold = delayThresholds[wdmsg.token]; + if (delayThreshold > 0 && wdmsg.amount > delayThreshold) { + _addDelayedTransfer(wdId, wdmsg.receiver, wdmsg.token, wdmsg.amount); + } else { + _sendToken(wdmsg.receiver, wdmsg.token, wdmsg.amount); + } + emit WithdrawDone(wdId, wdmsg.seqnum, wdmsg.receiver, wdmsg.token, wdmsg.amount, wdmsg.refid); + } + + function executeDelayedTransfer(bytes32 id) external whenNotPaused { + delayedTransfer memory transfer = _executeDelayedTransfer(id); + _sendToken(transfer.receiver, transfer.token, transfer.amount); + } + + function setMinAdd(address[] calldata _tokens, uint256[] calldata _amounts) external onlyGovernor { + require(_tokens.length == _amounts.length, "length mismatch"); + for (uint256 i = 0; i < _tokens.length; i++) { + minAdd[_tokens[i]] = _amounts[i]; + emit MinAddUpdated(_tokens[i], _amounts[i]); + } + } + + function _sendToken( + address _receiver, + address _token, + uint256 _amount + ) internal { + if (_token == nativeWrap) { + // withdraw then transfer native to receiver + IWETH(nativeWrap).withdraw(_amount); + (bool sent, ) = _receiver.call{value: _amount, gas: nativeTokenTransferGas}(""); + require(sent, "failed to send native token"); + } else { + IERC20(_token).safeTransfer(_receiver, _amount); + } + } + + // set nativeWrap, for relay requests, if token == nativeWrap, will withdraw first then transfer native to receiver + function setWrap(address _weth) external onlyOwner { + nativeWrap = _weth; + } + + // setNativeTransferGasUsed, native transfer will use this config. + function setNativeTokenTransferGas(uint256 _gasUsed) external onlyGovernor { + nativeTokenTransferGas = _gasUsed; + } +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/Signers.sol b/basic/80-crossChainTransfer/celerBridge/contracts/Signers.sol new file mode 100644 index 000000000..df418734e --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/Signers.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "./safeguard/Ownable.sol"; +import "./interfaces/ISigsVerifier.sol"; + +/** + * @title Multi-sig verification and management functions for {Bridge}. + */ +contract Signers is Ownable, ISigsVerifier { + using ECDSA for bytes32; + + bytes32 public ssHash; + uint256 public triggerTime; // timestamp when last update was triggered + + // reset can be called by the owner address for emergency recovery + uint256 public resetTime; + uint256 public noticePeriod; // advance notice period as seconds for reset + uint256 constant MAX_INT = 2**256 - 1; + + event SignersUpdated(address[] _signers, uint256[] _powers); + + event ResetNotification(uint256 resetTime); + + /** + * @notice Verifies that a message is signed by a quorum among the signers + * The sigs must be sorted by signer addresses in ascending order. + * @param _msg signed message + * @param _sigs list of signatures sorted by signer addresses in ascending order + * @param _signers sorted list of current signers + * @param _powers powers of current signers + */ + function verifySigs( + bytes memory _msg, + bytes[] calldata _sigs, + address[] calldata _signers, + uint256[] calldata _powers + ) public view override { + bytes32 h = keccak256(abi.encodePacked(_signers, _powers)); + require(ssHash == h, "Mismatch current signers"); + _verifySignedPowers(keccak256(_msg).toEthSignedMessageHash(), _sigs, _signers, _powers); + } + + /** + * @notice Update new signers. + * @param _newSigners sorted list of new signers + * @param _curPowers powers of new signers + * @param _sigs list of signatures sorted by signer addresses in ascending order + * @param _curSigners sorted list of current signers + * @param _curPowers powers of current signers + */ + function updateSigners( + uint256 _triggerTime, + address[] calldata _newSigners, + uint256[] calldata _newPowers, + bytes[] calldata _sigs, + address[] calldata _curSigners, + uint256[] calldata _curPowers + ) external { + // use trigger time for nonce protection, must be ascending + require(_triggerTime > triggerTime, "Trigger time is not increasing"); + // make sure triggerTime is not too large, as it cannot be decreased once set + require(_triggerTime < block.timestamp + 3600, "Trigger time is too large"); + bytes32 domain = keccak256(abi.encodePacked(block.chainid, address(this), "UpdateSigners")); + verifySigs(abi.encodePacked(domain, _triggerTime, _newSigners, _newPowers), _sigs, _curSigners, _curPowers); + _updateSigners(_newSigners, _newPowers); + triggerTime = _triggerTime; + } + + /** + * @notice reset signers, only used for init setup and emergency recovery + */ + function resetSigners(address[] calldata _signers, uint256[] calldata _powers) external onlyOwner { + require(block.timestamp > resetTime, "not reach reset time"); + resetTime = MAX_INT; + _updateSigners(_signers, _powers); + } + + function notifyResetSigners() external onlyOwner { + resetTime = block.timestamp + noticePeriod; + emit ResetNotification(resetTime); + } + + function increaseNoticePeriod(uint256 period) external onlyOwner { + require(period > noticePeriod, "notice period can only be increased"); + noticePeriod = period; + } + + // separate from verifySigs func to avoid "stack too deep" issue + function _verifySignedPowers( + bytes32 _hash, + bytes[] calldata _sigs, + address[] calldata _signers, + uint256[] calldata _powers + ) private pure { + require(_signers.length == _powers.length, "signers and powers length not match"); + uint256 totalPower; // sum of all signer.power + for (uint256 i = 0; i < _signers.length; i++) { + totalPower += _powers[i]; + } + uint256 quorum = (totalPower * 2) / 3 + 1; + + uint256 signedPower; // sum of signer powers who are in sigs + address prev = address(0); + uint256 index = 0; + for (uint256 i = 0; i < _sigs.length; i++) { + address signer = _hash.recover(_sigs[i]); + require(signer > prev, "signers not in ascending order"); + prev = signer; + // now find match signer add its power + while (signer > _signers[index]) { + index += 1; + require(index < _signers.length, "signer not found"); + } + if (signer == _signers[index]) { + signedPower += _powers[index]; + } + if (signedPower >= quorum) { + // return early to save gas + return; + } + } + revert("quorum not reached"); + } + + function _updateSigners(address[] calldata _signers, uint256[] calldata _powers) private { + require(_signers.length == _powers.length, "signers and powers length not match"); + address prev = address(0); + for (uint256 i = 0; i < _signers.length; i++) { + require(_signers[i] > prev, "New signers not in ascending order"); + prev = _signers[i]; + } + ssHash = keccak256(abi.encodePacked(_signers, _powers)); + emit SignersUpdated(_signers, _powers); + } +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/interfaces/ISigsVerifier.sol b/basic/80-crossChainTransfer/celerBridge/contracts/interfaces/ISigsVerifier.sol new file mode 100644 index 000000000..8297d4c9d --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/interfaces/ISigsVerifier.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity >=0.8.0; + +interface ISigsVerifier { + /** + * @notice Verifies that a message is signed by a quorum among the signers. + * @param _msg signed message + * @param _sigs list of signatures sorted by signer addresses in ascending order + * @param _signers sorted list of current signers + * @param _powers powers of current signers + */ + function verifySigs( + bytes memory _msg, + bytes[] calldata _sigs, + address[] calldata _signers, + uint256[] calldata _powers + ) external view; +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/interfaces/IWETH.sol b/basic/80-crossChainTransfer/celerBridge/contracts/interfaces/IWETH.sol new file mode 100644 index 000000000..89f49e932 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/interfaces/IWETH.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity >=0.8.0; + +interface IWETH { + function deposit() external payable; + + function withdraw(uint256) external; +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/libraries/Pb.sol b/basic/80-crossChainTransfer/celerBridge/contracts/libraries/Pb.sol new file mode 100644 index 000000000..a9484218d --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/libraries/Pb.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity 0.8.17; + +// runtime proto sol library +library Pb { + enum WireType { + Varint, + Fixed64, + LengthDelim, + StartGroup, + EndGroup, + Fixed32 + } + + struct Buffer { + uint256 idx; // the start index of next read. when idx=b.length, we're done + bytes b; // hold serialized proto msg, readonly + } + + // create a new in-memory Buffer object from raw msg bytes + function fromBytes(bytes memory raw) internal pure returns (Buffer memory buf) { + buf.b = raw; + buf.idx = 0; + } + + // whether there are unread bytes + function hasMore(Buffer memory buf) internal pure returns (bool) { + return buf.idx < buf.b.length; + } + + // decode current field number and wiretype + function decKey(Buffer memory buf) internal pure returns (uint256 tag, WireType wiretype) { + uint256 v = decVarint(buf); + tag = v / 8; + wiretype = WireType(v & 7); + } + + // count tag occurrences, return an array due to no memory map support + // have to create array for (maxtag+1) size. cnts[tag] = occurrences + // should keep buf.idx unchanged because this is only a count function + function cntTags(Buffer memory buf, uint256 maxtag) internal pure returns (uint256[] memory cnts) { + uint256 originalIdx = buf.idx; + cnts = new uint256[](maxtag + 1); // protobuf's tags are from 1 rather than 0 + uint256 tag; + WireType wire; + while (hasMore(buf)) { + (tag, wire) = decKey(buf); + cnts[tag] += 1; + skipValue(buf, wire); + } + buf.idx = originalIdx; + } + + // read varint from current buf idx, move buf.idx to next read, return the int value + function decVarint(Buffer memory buf) internal pure returns (uint256 v) { + bytes10 tmp; // proto int is at most 10 bytes (7 bits can be used per byte) + bytes memory bb = buf.b; // get buf.b mem addr to use in assembly + v = buf.idx; // use v to save one additional uint variable + assembly { + tmp := mload(add(add(bb, 32), v)) // load 10 bytes from buf.b[buf.idx] to tmp + } + uint256 b; // store current byte content + v = 0; // reset to 0 for return value + for (uint256 i = 0; i < 10; i++) { + assembly { + b := byte(i, tmp) // don't use tmp[i] because it does bound check and costs extra + } + v |= (b & 0x7F) << (i * 7); + if (b & 0x80 == 0) { + buf.idx += i + 1; + return v; + } + } + revert(); // i=10, invalid varint stream + } + + // read length delimited field and return bytes + function decBytes(Buffer memory buf) internal pure returns (bytes memory b) { + uint256 len = decVarint(buf); + uint256 end = buf.idx + len; + require(end <= buf.b.length); // avoid overflow + b = new bytes(len); + bytes memory bufB = buf.b; // get buf.b mem addr to use in assembly + uint256 bStart; + uint256 bufBStart = buf.idx; + assembly { + bStart := add(b, 32) + bufBStart := add(add(bufB, 32), bufBStart) + } + for (uint256 i = 0; i < len; i += 32) { + assembly { + mstore(add(bStart, i), mload(add(bufBStart, i))) + } + } + buf.idx = end; + } + + // return packed ints + function decPacked(Buffer memory buf) internal pure returns (uint256[] memory t) { + uint256 len = decVarint(buf); + uint256 end = buf.idx + len; + require(end <= buf.b.length); // avoid overflow + // array in memory must be init w/ known length + // so we have to create a tmp array w/ max possible len first + uint256[] memory tmp = new uint256[](len); + uint256 i = 0; // count how many ints are there + while (buf.idx < end) { + tmp[i] = decVarint(buf); + i++; + } + t = new uint256[](i); // init t with correct length + for (uint256 j = 0; j < i; j++) { + t[j] = tmp[j]; + } + return t; + } + + // move idx pass current value field, to beginning of next tag or msg end + function skipValue(Buffer memory buf, WireType wire) internal pure { + if (wire == WireType.Varint) { + decVarint(buf); + } else if (wire == WireType.LengthDelim) { + uint256 len = decVarint(buf); + buf.idx += len; // skip len bytes value data + require(buf.idx <= buf.b.length); // avoid overflow + } else { + revert(); + } // unsupported wiretype + } + + // type conversion help utils + function _bool(uint256 x) internal pure returns (bool v) { + return x != 0; + } + + function _uint256(bytes memory b) internal pure returns (uint256 v) { + require(b.length <= 32); // b's length must be smaller than or equal to 32 + assembly { + v := mload(add(b, 32)) + } // load all 32bytes to v + v = v >> (8 * (32 - b.length)); // only first b.length is valid + } + + function _address(bytes memory b) internal pure returns (address v) { + v = _addressPayable(b); + } + + function _addressPayable(bytes memory b) internal pure returns (address payable v) { + require(b.length == 20); + //load 32bytes then shift right 12 bytes + assembly { + v := div(mload(add(b, 32)), 0x1000000000000000000000000) + } + } + + function _bytes32(bytes memory b) internal pure returns (bytes32 v) { + require(b.length == 32); + assembly { + v := mload(add(b, 32)) + } + } + + // uint[] to uint8[] + function uint8s(uint256[] memory arr) internal pure returns (uint8[] memory t) { + t = new uint8[](arr.length); + for (uint256 i = 0; i < t.length; i++) { + t[i] = uint8(arr[i]); + } + } + + function uint32s(uint256[] memory arr) internal pure returns (uint32[] memory t) { + t = new uint32[](arr.length); + for (uint256 i = 0; i < t.length; i++) { + t[i] = uint32(arr[i]); + } + } + + function uint64s(uint256[] memory arr) internal pure returns (uint64[] memory t) { + t = new uint64[](arr.length); + for (uint256 i = 0; i < t.length; i++) { + t[i] = uint64(arr[i]); + } + } + + function bools(uint256[] memory arr) internal pure returns (bool[] memory t) { + t = new bool[](arr.length); + for (uint256 i = 0; i < t.length; i++) { + t[i] = arr[i] != 0; + } + } +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/libraries/PbBridge.sol b/basic/80-crossChainTransfer/celerBridge/contracts/libraries/PbBridge.sol new file mode 100644 index 000000000..456968ec2 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/libraries/PbBridge.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// Code generated by protoc-gen-sol. DO NOT EDIT. +// source: bridge.proto +pragma solidity 0.8.17; +import "./Pb.sol"; + +library PbBridge { + using Pb for Pb.Buffer; // so we can call Pb funcs on Buffer obj + + struct Relay { + address sender; // tag: 1 + address receiver; // tag: 2 + address token; // tag: 3 + uint256 amount; // tag: 4 + uint64 srcChainId; // tag: 5 + uint64 dstChainId; // tag: 6 + bytes32 srcTransferId; // tag: 7 + } // end struct Relay + + function decRelay(bytes memory raw) internal pure returns (Relay memory m) { + Pb.Buffer memory buf = Pb.fromBytes(raw); + + uint256 tag; + Pb.WireType wire; + while (buf.hasMore()) { + (tag, wire) = buf.decKey(); + if (false) {} + // solidity has no switch/case + else if (tag == 1) { + m.sender = Pb._address(buf.decBytes()); + } else if (tag == 2) { + m.receiver = Pb._address(buf.decBytes()); + } else if (tag == 3) { + m.token = Pb._address(buf.decBytes()); + } else if (tag == 4) { + m.amount = Pb._uint256(buf.decBytes()); + } else if (tag == 5) { + m.srcChainId = uint64(buf.decVarint()); + } else if (tag == 6) { + m.dstChainId = uint64(buf.decVarint()); + } else if (tag == 7) { + m.srcTransferId = Pb._bytes32(buf.decBytes()); + } else { + buf.skipValue(wire); + } // skip value of unknown tag + } + } // end decoder Relay +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/libraries/PbPegged.sol b/basic/80-crossChainTransfer/celerBridge/contracts/libraries/PbPegged.sol new file mode 100644 index 000000000..3d2180836 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/libraries/PbPegged.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// Code generated by protoc-gen-sol. DO NOT EDIT. +// source: contracts/libraries/proto/pegged.proto +pragma solidity 0.8.17; +import "./Pb.sol"; + +library PbPegged { + using Pb for Pb.Buffer; // so we can call Pb funcs on Buffer obj + + struct Mint { + address token; // tag: 1 + address account; // tag: 2 + uint256 amount; // tag: 3 + address depositor; // tag: 4 + uint64 refChainId; // tag: 5 + bytes32 refId; // tag: 6 + } // end struct Mint + + function decMint(bytes memory raw) internal pure returns (Mint memory m) { + Pb.Buffer memory buf = Pb.fromBytes(raw); + + uint256 tag; + Pb.WireType wire; + while (buf.hasMore()) { + (tag, wire) = buf.decKey(); + if (false) {} + // solidity has no switch/case + else if (tag == 1) { + m.token = Pb._address(buf.decBytes()); + } else if (tag == 2) { + m.account = Pb._address(buf.decBytes()); + } else if (tag == 3) { + m.amount = Pb._uint256(buf.decBytes()); + } else if (tag == 4) { + m.depositor = Pb._address(buf.decBytes()); + } else if (tag == 5) { + m.refChainId = uint64(buf.decVarint()); + } else if (tag == 6) { + m.refId = Pb._bytes32(buf.decBytes()); + } else { + buf.skipValue(wire); + } // skip value of unknown tag + } + } // end decoder Mint + + struct Withdraw { + address token; // tag: 1 + address receiver; // tag: 2 + uint256 amount; // tag: 3 + address burnAccount; // tag: 4 + uint64 refChainId; // tag: 5 + bytes32 refId; // tag: 6 + } // end struct Withdraw + + function decWithdraw(bytes memory raw) internal pure returns (Withdraw memory m) { + Pb.Buffer memory buf = Pb.fromBytes(raw); + + uint256 tag; + Pb.WireType wire; + while (buf.hasMore()) { + (tag, wire) = buf.decKey(); + if (false) {} + // solidity has no switch/case + else if (tag == 1) { + m.token = Pb._address(buf.decBytes()); + } else if (tag == 2) { + m.receiver = Pb._address(buf.decBytes()); + } else if (tag == 3) { + m.amount = Pb._uint256(buf.decBytes()); + } else if (tag == 4) { + m.burnAccount = Pb._address(buf.decBytes()); + } else if (tag == 5) { + m.refChainId = uint64(buf.decVarint()); + } else if (tag == 6) { + m.refId = Pb._bytes32(buf.decBytes()); + } else { + buf.skipValue(wire); + } // skip value of unknown tag + } + } // end decoder Withdraw +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/libraries/PbPool.sol b/basic/80-crossChainTransfer/celerBridge/contracts/libraries/PbPool.sol new file mode 100644 index 000000000..211d6ad08 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/libraries/PbPool.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only + +// Code generated by protoc-gen-sol. DO NOT EDIT. +// source: contracts/libraries/proto/pool.proto +pragma solidity 0.8.17; +import "./Pb.sol"; + +library PbPool { + using Pb for Pb.Buffer; // so we can call Pb funcs on Buffer obj + + struct WithdrawMsg { + uint64 chainid; // tag: 1 + uint64 seqnum; // tag: 2 + address receiver; // tag: 3 + address token; // tag: 4 + uint256 amount; // tag: 5 + bytes32 refid; // tag: 6 + } // end struct WithdrawMsg + + function decWithdrawMsg(bytes memory raw) internal pure returns (WithdrawMsg memory m) { + Pb.Buffer memory buf = Pb.fromBytes(raw); + + uint256 tag; + Pb.WireType wire; + while (buf.hasMore()) { + (tag, wire) = buf.decKey(); + if (false) {} + // solidity has no switch/case + else if (tag == 1) { + m.chainid = uint64(buf.decVarint()); + } else if (tag == 2) { + m.seqnum = uint64(buf.decVarint()); + } else if (tag == 3) { + m.receiver = Pb._address(buf.decBytes()); + } else if (tag == 4) { + m.token = Pb._address(buf.decBytes()); + } else if (tag == 5) { + m.amount = Pb._uint256(buf.decBytes()); + } else if (tag == 6) { + m.refid = Pb._bytes32(buf.decBytes()); + } else { + buf.skipValue(wire); + } // skip value of unknown tag + } + } // end decoder WithdrawMsg +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/DelayedTransfer.sol b/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/DelayedTransfer.sol new file mode 100644 index 000000000..0ca0c0394 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/DelayedTransfer.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity 0.8.17; + +import "./Governor.sol"; + +abstract contract DelayedTransfer is Governor { + struct delayedTransfer { + address receiver; + address token; + uint256 amount; + uint256 timestamp; + } + mapping(bytes32 => delayedTransfer) public delayedTransfers; + mapping(address => uint256) public delayThresholds; + uint256 public delayPeriod; // in seconds + + event DelayedTransferAdded(bytes32 id); + event DelayedTransferExecuted(bytes32 id, address receiver, address token, uint256 amount); + + event DelayPeriodUpdated(uint256 period); + event DelayThresholdUpdated(address token, uint256 threshold); + + function setDelayThresholds(address[] calldata _tokens, uint256[] calldata _thresholds) external onlyGovernor { + require(_tokens.length == _thresholds.length, "length mismatch"); + for (uint256 i = 0; i < _tokens.length; i++) { + delayThresholds[_tokens[i]] = _thresholds[i]; + emit DelayThresholdUpdated(_tokens[i], _thresholds[i]); + } + } + + function setDelayPeriod(uint256 _period) external onlyGovernor { + delayPeriod = _period; + emit DelayPeriodUpdated(_period); + } + + function _addDelayedTransfer( + bytes32 id, + address receiver, + address token, + uint256 amount + ) internal { + require(delayedTransfers[id].timestamp == 0, "delayed transfer already exists"); + delayedTransfers[id] = delayedTransfer({ + receiver: receiver, + token: token, + amount: amount, + timestamp: block.timestamp + }); + emit DelayedTransferAdded(id); + } + + // caller needs to do the actual token transfer + function _executeDelayedTransfer(bytes32 id) internal returns (delayedTransfer memory) { + delayedTransfer memory transfer = delayedTransfers[id]; + require(transfer.timestamp > 0, "delayed transfer not exist"); + require(block.timestamp > transfer.timestamp + delayPeriod, "delayed transfer still locked"); + delete delayedTransfers[id]; + emit DelayedTransferExecuted(id, transfer.receiver, transfer.token, transfer.amount); + return transfer; + } +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/Governor.sol b/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/Governor.sol new file mode 100644 index 000000000..f1730a419 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/Governor.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity 0.8.17; + +import "./Ownable.sol"; + +abstract contract Governor is Ownable { + mapping(address => bool) public governors; + + event GovernorAdded(address account); + event GovernorRemoved(address account); + + modifier onlyGovernor() { + require(isGovernor(msg.sender), "Caller is not governor"); + _; + } + + constructor() { + _addGovernor(msg.sender); + } + + function isGovernor(address _account) public view returns (bool) { + return governors[_account]; + } + + function addGovernor(address _account) public onlyOwner { + _addGovernor(_account); + } + + function removeGovernor(address _account) public onlyOwner { + _removeGovernor(_account); + } + + function renounceGovernor() public { + _removeGovernor(msg.sender); + } + + function _addGovernor(address _account) private { + require(!isGovernor(_account), "Account is already governor"); + governors[_account] = true; + emit GovernorAdded(_account); + } + + function _removeGovernor(address _account) private { + require(isGovernor(_account), "Account is not governor"); + governors[_account] = false; + emit GovernorRemoved(_account); + } +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/Ownable.sol b/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/Ownable.sol new file mode 100644 index 000000000..94b1f7b0f --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/Ownable.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.0; + +/** + * @dev Contract module which provides a basic access control mechanism, where + * there is an account (an owner) that can be granted exclusive access to + * specific functions. + * + * By default, the owner account will be the one that deploys the contract. This + * can later be changed with {transferOwnership}. + * + * This module is used through inheritance. It will make available the modifier + * `onlyOwner`, which can be applied to your functions to restrict their use to + * the owner. + * + * This adds a normal func that setOwner if _owner is address(0). So we can't allow + * renounceOwnership. So we can support Proxy based upgradable contract + */ +abstract contract Ownable { + address private _owner; + + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor() { + _setOwner(msg.sender); + } + + /** + * @dev Only to be called by inherit contracts, in their init func called by Proxy + * we require _owner == address(0), which is only possible when it's a delegateCall + * because constructor sets _owner in contract state. + */ + function initOwner() internal { + require(_owner == address(0), "owner already set"); + _setOwner(msg.sender); + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + require(owner() == msg.sender, "Ownable: caller is not the owner"); + _; + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + require(newOwner != address(0), "Ownable: new owner is the zero address"); + _setOwner(newOwner); + } + + function _setOwner(address newOwner) private { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/Pauser.sol b/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/Pauser.sol new file mode 100644 index 000000000..f7105b328 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/Pauser.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity 0.8.17; + +import "@openzeppelin/contracts/security/Pausable.sol"; +import "./Ownable.sol"; + +abstract contract Pauser is Ownable, Pausable { + mapping(address => bool) public pausers; + + event PauserAdded(address account); + event PauserRemoved(address account); + + constructor() { + _addPauser(msg.sender); + } + + modifier onlyPauser() { + require(isPauser(msg.sender), "Caller is not pauser"); + _; + } + + function pause() public onlyPauser { + _pause(); + } + + function unpause() public onlyPauser { + _unpause(); + } + + function isPauser(address account) public view returns (bool) { + return pausers[account]; + } + + function addPauser(address account) public onlyOwner { + _addPauser(account); + } + + function removePauser(address account) public onlyOwner { + _removePauser(account); + } + + function renouncePauser() public { + _removePauser(msg.sender); + } + + function _addPauser(address account) private { + require(!isPauser(account), "Account is already pauser"); + pausers[account] = true; + emit PauserAdded(account); + } + + function _removePauser(address account) private { + require(isPauser(account), "Account is not pauser"); + pausers[account] = false; + emit PauserRemoved(account); + } +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/VolumeControl.sol b/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/VolumeControl.sol new file mode 100644 index 000000000..7c8348d1a --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/contracts/safeguard/VolumeControl.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity 0.8.17; + +import "./Governor.sol"; + +abstract contract VolumeControl is Governor { + uint256 public epochLength; // seconds + mapping(address => uint256) public epochVolumes; // key is token + mapping(address => uint256) public epochVolumeCaps; // key is token + mapping(address => uint256) public lastOpTimestamps; // key is token + + event EpochLengthUpdated(uint256 length); + event EpochVolumeUpdated(address token, uint256 cap); + + function setEpochLength(uint256 _length) external onlyGovernor { + epochLength = _length; + emit EpochLengthUpdated(_length); + } + + function setEpochVolumeCaps(address[] calldata _tokens, uint256[] calldata _caps) external onlyGovernor { + require(_tokens.length == _caps.length, "length mismatch"); + for (uint256 i = 0; i < _tokens.length; i++) { + epochVolumeCaps[_tokens[i]] = _caps[i]; + emit EpochVolumeUpdated(_tokens[i], _caps[i]); + } + } + + function _updateVolume(address _token, uint256 _amount) internal { + if (epochLength == 0) { + return; + } + uint256 cap = epochVolumeCaps[_token]; + if (cap == 0) { + return; + } + uint256 volume = epochVolumes[_token]; + uint256 timestamp = block.timestamp; + uint256 epochStartTime = (timestamp / epochLength) * epochLength; + if (lastOpTimestamps[_token] < epochStartTime) { + volume = _amount; + } else { + volume += _amount; + } + require(volume <= cap, "volume exceeds cap"); + epochVolumes[_token] = volume; + lastOpTimestamps[_token] = timestamp; + } +} \ No newline at end of file diff --git a/basic/80-crossChainTransfer/celerBridge/hardhat.config.js b/basic/80-crossChainTransfer/celerBridge/hardhat.config.js new file mode 100644 index 000000000..3a32c008a --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/hardhat.config.js @@ -0,0 +1,64 @@ +require("@nomiclabs/hardhat-waffle"); +require('dotenv').config(); + +const settings = { + optimizer: { + enabled: true, + runs: 200, + }, +}; + +function mnemonic() { + return [process.env.PRIVATE_KEY, process.env.PRIVATE_KEY1, process.env.PRIVATE_KEY2]; +} + +/** @type import('hardhat/config').HardhatUserConfig */ +module.exports = { + solidity: { + compilers: [ + { version: '0.8.17', settings }, + { version: '0.8.9', settings }, + ], + }, + networks: { + localhost: { + url: 'http://localhost:8545', + //gasPrice: 125000000000, // you can adjust gasPrice locally to see how much it will cost on production + /* + notice no mnemonic here? it will just use account 0 of the hardhat node to deploy + (you can put in a mnemonic here to set the deployer locally) + */ + }, + mainnet: { + url: 'https://mainnet.infura.io/v3/' + process.env.INFURA_ID, //<---- YOUR INFURA ID! (or it won't work) + accounts: mnemonic(), + }, + matic: { + url: 'https://polygon-mainnet.infura.io/v3/' + process.env.INFURA_ID, + accounts: mnemonic() + }, + optim: { + url: "https://optimism-mainnet.infura.io/v3/" + process.env.INFURA_ID, + accounts: mnemonic() + }, + sepolia: { + url: "https://sepolia.infura.io/v3/" + process.env.INFURA_ID, + accounts: mnemonic() + }, + arbitrum: { + url: "https://arbitrum-mainnet.infura.io/v3/" + process.env.INFURA_ID, + accounts: mnemonic() + }, + scroll: { + url: "https://rpc.scroll.io", + accounts: mnemonic() + }, + mumbai:{ + url: "https://polygon-mumbai.infura.io/v3/" + process.env.INFURA_ID, + accounts: mnemonic() + } + }, + mocha: { + timeout: 20000 + }, +}; diff --git a/basic/80-crossChainTransfer/celerBridge/package.json b/basic/80-crossChainTransfer/celerBridge/package.json new file mode 100644 index 000000000..8c3e2de2e --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/package.json @@ -0,0 +1,25 @@ +{ + "name": "celerbridge", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@openzeppelin/contracts": "^4.5.0", + "dotenv": "^16.4.5", + "hardhat": "^2.9.0", + "node-fetch": "^2.7.0" + }, + "devDependencies": { + "@nomiclabs/hardhat-ethers": "^2.2.3", + "@nomiclabs/hardhat-waffle": "^2.0.6", + "chai": "^4.4.1", + "ethereum-waffle": "^3.4.4", + "ethers": "^5.7.2" + } +} diff --git a/basic/80-crossChainTransfer/celerBridge/scripts/1-poolBasedTransfer.js b/basic/80-crossChainTransfer/celerBridge/scripts/1-poolBasedTransfer.js new file mode 100644 index 000000000..7b0057488 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/scripts/1-poolBasedTransfer.js @@ -0,0 +1,102 @@ +// We require the Hardhat Runtime Environment explicitly here. This is optional +// but useful for running the script in a standalone fashion through `node