From 01ecd78d7cdf1d554c3a2a45996945ae71e3a116 Mon Sep 17 00:00:00 2001 From: gongtao245 Date: Tue, 27 Feb 2024 21:38:00 +0800 Subject: [PATCH 1/3] add chainlink CCIP && Celer bridge --- .../celerBridge/.env.example | 8 + .../celerBridge/.gitignore | 9 + .../celerBridge/README-cn.md | 49 + .../celerBridge/README.md | 5 + .../celerBridge/contracts/Bridge.sol | 189 + .../contracts/OriginalTokenVault.sol | 204 + .../celerBridge/contracts/Pool.sol | 150 + .../celerBridge/contracts/Signers.sol | 138 + .../contracts/interfaces/ISigsVerifier.sol | 19 + .../contracts/interfaces/IWETH.sol | 9 + .../celerBridge/contracts/libraries/Pb.sol | 192 + .../contracts/libraries/PbBridge.sol | 49 + .../contracts/libraries/PbPegged.sol | 82 + .../contracts/libraries/PbPool.sol | 46 + .../contracts/safeguard/DelayedTransfer.sol | 62 + .../contracts/safeguard/Governor.sol | 49 + .../contracts/safeguard/Ownable.sol | 71 + .../contracts/safeguard/Pauser.sol | 58 + .../contracts/safeguard/VolumeControl.sol | 49 + .../celerBridge/hardhat.config.js | 64 + .../celerBridge/package.json | 25 + .../scripts/1-poolBasedTransfer.js | 103 + .../scripts/2-queryPoolBasedTrasnferStatus.js | 97 + .../scripts/3-canonicalTokenTransfer.js | 55 + .../scripts/4-queryCanonicalTrasnferStatus.js | 98 + .../scripts/5-canonicalTrasnferRefund.js | 126 + .../celerBridge/scripts/deployment.json | 1 + .../celerBridge/utils/index.js | 113 + .../chainlinkCCIP/.env.example | 8 + .../chainlinkCCIP/.gitignore | 50 + .../chainlinkCCIP/README-cn.md | 104 + .../chainlinkCCIP/README.md | 104 + .../chainlinkCCIP/contracts/Lock.sol | 34 + .../contracts/ProgrammableTokenTransfers.sol | 388 ++ .../chainlinkCCIP/contracts/Receiver.sol | 55 + .../chainlinkCCIP/contracts/Sender.sol | 94 + .../chainlinkCCIP/contracts/SimpleToken.sol | 3307 +++++++++++++++++ .../chainlinkCCIP/hardhat.config.js | 81 + .../chainlinkCCIP/package.json | 11 + .../chainlinkCCIP/scripts/deployment.json | 1 + .../1-deploySenderOnSepolia.js | 37 + .../2-transferLinkToSenderOnSepolia.js | 27 + .../3-deployReceiverOnMumbai.js | 36 + ...dCrossChainDataOnSepolia_PayByLinkToken.js | 35 + .../5-receiveCrossChainDataOnMumbai.js | 30 + .../1-deployTokenTransferorOnSepolia.js | 43 + ...-transferLinkToTokenTransferorOnSepolia.js | 32 + .../3-deployTokenTransferorOnMumbai.js | 52 + ...CrossChainTokenOnSepolia_PayByLinkToken.js | 37 + ...-checkResultOnReceiveCrossChainOnMumbai.js | 26 + .../chainlinkCCIP/test/LockSample.js | 126 + .../chainlinkCCIP/utils/index.js | 113 + .../circle-cctp}/.env.example | 0 .../circle-cctp}/.gitignore | 0 .../circle-cctp}/README-cn.md | 0 .../circle-cctp}/README.md | 0 .../circle-cctp}/package.json | 0 .../circle-cctp}/pnpm-lock.yaml | 0 .../circle-cctp}/src/usdc_transfer_cctp.ts | 0 .../circle-cctp}/tsconfig.json | 0 60 files changed, 6951 insertions(+) create mode 100644 basic/80-crossChainTransfer/celerBridge/.env.example create mode 100644 basic/80-crossChainTransfer/celerBridge/.gitignore create mode 100644 basic/80-crossChainTransfer/celerBridge/README-cn.md create mode 100644 basic/80-crossChainTransfer/celerBridge/README.md create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/Bridge.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/OriginalTokenVault.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/Pool.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/Signers.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/interfaces/ISigsVerifier.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/interfaces/IWETH.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/libraries/Pb.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/libraries/PbBridge.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/libraries/PbPegged.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/libraries/PbPool.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/safeguard/DelayedTransfer.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/safeguard/Governor.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/safeguard/Ownable.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/safeguard/Pauser.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/contracts/safeguard/VolumeControl.sol create mode 100644 basic/80-crossChainTransfer/celerBridge/hardhat.config.js create mode 100644 basic/80-crossChainTransfer/celerBridge/package.json create mode 100644 basic/80-crossChainTransfer/celerBridge/scripts/1-poolBasedTransfer.js create mode 100644 basic/80-crossChainTransfer/celerBridge/scripts/2-queryPoolBasedTrasnferStatus.js create mode 100644 basic/80-crossChainTransfer/celerBridge/scripts/3-canonicalTokenTransfer.js create mode 100644 basic/80-crossChainTransfer/celerBridge/scripts/4-queryCanonicalTrasnferStatus.js create mode 100644 basic/80-crossChainTransfer/celerBridge/scripts/5-canonicalTrasnferRefund.js create mode 100644 basic/80-crossChainTransfer/celerBridge/scripts/deployment.json create mode 100644 basic/80-crossChainTransfer/celerBridge/utils/index.js create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/.env.example create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/.gitignore create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/README-cn.md create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/README.md create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/contracts/Lock.sol create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/contracts/ProgrammableTokenTransfers.sol create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/contracts/Receiver.sol create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/contracts/Sender.sol create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/contracts/SimpleToken.sol create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/hardhat.config.js create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/package.json create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/scripts/deployment.json create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/scripts/sendCrossChainData/1-deploySenderOnSepolia.js create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/scripts/sendCrossChainData/2-transferLinkToSenderOnSepolia.js create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/scripts/sendCrossChainData/3-deployReceiverOnMumbai.js create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/scripts/sendCrossChainData/4-sendCrossChainDataOnSepolia_PayByLinkToken.js create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/scripts/sendCrossChainData/5-receiveCrossChainDataOnMumbai.js create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/scripts/sendCrossChainToken/1-deployTokenTransferorOnSepolia.js create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/scripts/sendCrossChainToken/2-transferLinkToTokenTransferorOnSepolia.js create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/scripts/sendCrossChainToken/3-deployTokenTransferorOnMumbai.js create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/scripts/sendCrossChainToken/4-sendCrossChainTokenOnSepolia_PayByLinkToken.js create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/scripts/sendCrossChainToken/5-checkResultOnReceiveCrossChainOnMumbai.js create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/test/LockSample.js create mode 100644 basic/80-crossChainTransfer/chainlinkCCIP/utils/index.js rename basic/{80-circle-cctp => 80-crossChainTransfer/circle-cctp}/.env.example (100%) rename basic/{80-circle-cctp => 80-crossChainTransfer/circle-cctp}/.gitignore (100%) rename basic/{80-circle-cctp => 80-crossChainTransfer/circle-cctp}/README-cn.md (100%) rename basic/{80-circle-cctp => 80-crossChainTransfer/circle-cctp}/README.md (100%) rename basic/{80-circle-cctp => 80-crossChainTransfer/circle-cctp}/package.json (100%) rename basic/{80-circle-cctp => 80-crossChainTransfer/circle-cctp}/pnpm-lock.yaml (100%) rename basic/{80-circle-cctp => 80-crossChainTransfer/circle-cctp}/src/usdc_transfer_cctp.ts (100%) rename basic/{80-circle-cctp => 80-crossChainTransfer/circle-cctp}/tsconfig.json (100%) 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..36077f288 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/.gitignore @@ -0,0 +1,9 @@ +node_modules +.env +coverage +coverage.json +typechain + +#Hardhat files +cache +artifacts diff --git a/basic/80-crossChainTransfer/celerBridge/README-cn.md b/basic/80-crossChainTransfer/celerBridge/README-cn.md new file mode 100644 index 000000000..f6d624eda --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/README-cn.md @@ -0,0 +1,49 @@ +# Celer Bridge 跨链 +中文 / [English](./README.md) + +## 概览 +cBridge 引入了最佳的跨链代币桥接体验,为用户提供了深度流动性,为 cBridge 节点运营商和不想运营 cBridge 节点的流动性提供者提供了高效且易于使用的流动性管理,以及新的令人兴奋的面向开发者的功能,如用于跨链 DEX 和 NFT 等场景的通用消息桥接。 + +本测试将以 OP mainnet 和 Polygon mainnet 进行测试 + +## 准备测试环境 +- 安装依赖 +``` +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 +``` + +- 以映射方式进行跨链 +``` +## 为测试需要,本次跨链将会失败,OP tonken 会被 unlock,为后续的 refund 做准备 +npx hardhat run scripts/3-canonicalTokenTransfer.js --network optim +``` + +- 检查跨链结果 +``` +npx hardhat run scripts/4-queryCanonicalTrasnferStatus.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..678706365 --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/README.md @@ -0,0 +1,5 @@ +# Celer Bridge +[中文](./README-cn.md) / English + +## 概览 + 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..11f580f4a --- /dev/null +++ b/basic/80-crossChainTransfer/celerBridge/scripts/1-poolBasedTransfer.js @@ -0,0 +1,103 @@ +// We require the Hardhat Runtime Environment explicitly here. This is optional +// but useful for running the script in a standalone fashion through `node