From 1e651349e7bcb44a8913e0284bac6cc441f7b0dc Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 16 Oct 2023 13:35:15 -0400 Subject: [PATCH 01/42] updated chainlink packages --- contracts/core/SlashingKeeper.sol | 8 +++----- package.json | 3 ++- yarn.lock | 28 +++++++++++++++++++--------- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/contracts/core/SlashingKeeper.sol b/contracts/core/SlashingKeeper.sol index 1cf6bfe5..b00e0138 100644 --- a/contracts/core/SlashingKeeper.sol +++ b/contracts/core/SlashingKeeper.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.15; -import "@chainlink/contracts/src/v0.8/KeeperCompatible.sol"; - import "./interfaces/IStakingPool.sol"; import "./interfaces/IStrategy.sol"; @@ -10,7 +8,7 @@ import "./interfaces/IStrategy.sol"; * @title Slashing Keeper * @notice Updates strategy rewards if any losses have been incurred */ -contract SlashingKeeper is KeeperCompatibleInterface { +contract SlashingKeeper { IStakingPool public stakingPool; constructor(address _stakingPool) { @@ -22,7 +20,7 @@ contract SlashingKeeper is KeeperCompatibleInterface { * @return upkeepNeeded whether or not rewards should be updated * @return performData abi encoded list of strategy indexes to update **/ - function checkUpkeep(bytes calldata) external view override returns (bool, bytes memory) { + function checkUpkeep(bytes calldata) external view returns (bool, bytes memory) { address[] memory strategies = stakingPool.getStrategies(); bool[] memory strategiesToUpdate = new bool[](strategies.length); uint256 totalStrategiesToUpdate; @@ -56,7 +54,7 @@ contract SlashingKeeper is KeeperCompatibleInterface { * @notice Updates rewards * @param _performData abi encoded list of strategy indexes to update */ - function performUpkeep(bytes calldata _performData) external override { + function performUpkeep(bytes calldata _performData) external { address[] memory strategies = stakingPool.getStrategies(); uint256[] memory strategiesToUpdate = abi.decode(_performData, (uint256[])); require(strategiesToUpdate.length > 0, "No strategies to update"); diff --git a/package.json b/package.json index 3f394610..a4f426c0 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "typescript": "^4.5.5" }, "dependencies": { - "@chainlink/contracts": "0.6.1", + "@chainlink/contracts": "0.8.0", + "@chainlink/contracts-ccip": "^0.7.6", "@openzeppelin/contracts": "^4.7.0", "@openzeppelin/contracts-upgradeable": "^4.9.2", "@prb/math": "^2.5.0", diff --git a/yarn.lock b/yarn.lock index f0d6e28d..9038f44e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -201,14 +201,24 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" -"@chainlink/contracts@0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@chainlink/contracts/-/contracts-0.6.1.tgz#8842b57e755793cbdbcbc45277fb5d179c993e19" - integrity sha512-EuwijGexttw0UjfrW+HygwhQIrGAbqpf1ue28R55HhWMHBzphEH0PhWm8DQmFfj5OZNy8Io66N4L0nStkZ3QKQ== +"@chainlink/contracts-ccip@^0.7.6": + version "0.7.6" + resolved "https://registry.yarnpkg.com/@chainlink/contracts-ccip/-/contracts-ccip-0.7.6.tgz#5bf4568a0bbf4e29d2e8c32348e5ecc6ced006d2" + integrity sha512-yNbCBFpLs3R+ALymto9dQYKz3vatnjqYGu1pnMD0i2fHEMthiXe0+otaNCGNht6n8k7ruNaA0DNpz3F+2jHQXw== + dependencies: + "@eth-optimism/contracts" "^0.5.21" + "@openzeppelin/contracts" "~4.3.3" + "@openzeppelin/contracts-upgradeable-4.7.3" "npm:@openzeppelin/contracts-upgradeable@v4.7.3" + "@openzeppelin/contracts-v0.7" "npm:@openzeppelin/contracts@v3.4.2" + +"@chainlink/contracts@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@chainlink/contracts/-/contracts-0.8.0.tgz#4050c83c8b1603ffb0fd6ab99f1d9ea9db2c37de" + integrity sha512-nUv1Uxw5Mn92wgLs2bgPYmo8hpdQ3s9jB/lcbdU0LmNOVu0hbfmouVnqwRLa28Ll50q6GczUA+eO0ikNIKLZsA== dependencies: "@eth-optimism/contracts" "^0.5.21" "@openzeppelin/contracts" "~4.3.3" - "@openzeppelin/contracts-upgradeable" "^4.7.3" + "@openzeppelin/contracts-upgradeable-4.7.3" "npm:@openzeppelin/contracts-upgradeable@v4.7.3" "@openzeppelin/contracts-v0.7" "npm:@openzeppelin/contracts@v3.4.2" "@cspotcode/source-map-consumer@0.8.0": @@ -1615,10 +1625,10 @@ "@types/sinon-chai" "^3.2.3" "@types/web3" "1.0.19" -"@openzeppelin/contracts-upgradeable@^4.7.3": - version "4.9.3" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.9.3.tgz#ff17a80fb945f5102571f8efecb5ce5915cc4811" - integrity sha512-jjaHAVRMrE4UuZNfDwjlLGDxTHWIOwTJS2ldnc278a0gevfXfPr8hxKEVBGFBE96kl2G3VHDZhUimw/+G3TG2A== +"@openzeppelin/contracts-upgradeable-4.7.3@npm:@openzeppelin/contracts-upgradeable@v4.7.3": + version "4.7.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-4.7.3.tgz#f1d606e2827d409053f3e908ba4eb8adb1dd6995" + integrity sha512-+wuegAMaLcZnLCJIvrVUDzA9z/Wp93f0Dla/4jJvIhijRrPabjQbZe6fWiECLaJyfn5ci9fqf9vTw3xpQOad2A== "@openzeppelin/contracts-upgradeable@^4.9.2": version "4.9.2" From 58ba52576be4c650fddccc615da1f67458a49758 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Tue, 17 Oct 2023 09:38:53 -0400 Subject: [PATCH 02/42] ccip wrapped token bridge --- contracts/core/RewardsPoolWSD.sol | 8 +- contracts/core/ccip/WrappedTokenBridge.sol | 177 ++++++++++++++++++ .../{IWrappedSDToken.sol => IWrappedLST.sol} | 2 +- 3 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 contracts/core/ccip/WrappedTokenBridge.sol rename contracts/core/interfaces/{IWrappedSDToken.sol => IWrappedLST.sol} (94%) diff --git a/contracts/core/RewardsPoolWSD.sol b/contracts/core/RewardsPoolWSD.sol index 0bf6ea83..79424cf7 100644 --- a/contracts/core/RewardsPoolWSD.sol +++ b/contracts/core/RewardsPoolWSD.sol @@ -4,25 +4,25 @@ pragma solidity 0.8.15; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./interfaces/IRewardsPoolController.sol"; -import "./interfaces/IWrappedSDToken.sol"; +import "./interfaces/IWrappedLST.sol"; import "./RewardsPool.sol"; /** * @title RewardsPoolWSD - * @notice Handles reward distribution for a single wrapped staking derivative token + * @notice Handles reward distribution for a single wrapped liquid staking token * @dev rewards can only be positive (user balances can only increase) */ contract RewardsPoolWSD is RewardsPool { using SafeERC20 for IERC677; - IWrappedSDToken public wsdToken; + IWrappedLST public wsdToken; constructor( address _controller, address _token, address _wsdToken ) RewardsPool(_controller, _token) { - wsdToken = IWrappedSDToken(_wsdToken); + wsdToken = IWrappedLST(_wsdToken); } /** diff --git a/contracts/core/ccip/WrappedTokenBridge.sol b/contracts/core/ccip/WrappedTokenBridge.sol new file mode 100644 index 00000000..97767388 --- /dev/null +++ b/contracts/core/ccip/WrappedTokenBridge.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol"; +import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; +import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol"; +import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "../interfaces/IWrappedLST.sol"; + +contract WrappedTokenBridge is Ownable, CCIPReceiver { + LinkTokenInterface linkToken; + + IERC20 token; + IWrappedLST wrappedToken; + + event TokensTransferred( + bytes32 indexed messageId, + uint64 indexed destinationChainSelector, + address indexed sender, + address receiver, + uint256 tokenAmount, + address feeToken, + uint256 fees + ); + event TokensReceived( + bytes32 indexed messageId, + uint64 indexed destinationChainSelector, + address indexed sender, + address receiver, + uint256 tokenAmount + ); + + error InvalidSender(); + error InvalidValue(); + error InsufficientFee(); + error TransferFailed(); + + constructor( + address _router, + address _linkToken, + address _token, + address _wrappedToken + ) CCIPReceiver(_router) { + linkToken = LinkTokenInterface(_linkToken); + + token = IERC20(_token); + wrappedToken = IWrappedLST(_wrappedToken); + + linkToken.approve(_router, type(uint256).max); + token.approve(_wrappedToken, type(uint256).max); + wrappedToken.approve(_router, type(uint256).max); + } + + function onTokenTransfer( + address _sender, + uint256 _value, + bytes calldata _calldata + ) external returns (bytes32 messageId) { + if (msg.sender != address(token)) revert InvalidSender(); + if (_value == 0) revert InvalidValue(); + + uint256 preWrapBalance = wrappedToken.balanceOf(address(this)); + wrappedToken.wrap(_value); + uint256 amountToTransfer = wrappedToken.balanceOf(address(this)) - preWrapBalance; + + (uint64 destinationChainSelector, address receiver) = abi.decode(_calldata, (uint64, address)); + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(receiver, amountToTransfer, address(linkToken)); + + IRouterClient router = IRouterClient(this.getRouter()); + + uint256 fees = router.getFee(destinationChainSelector, evm2AnyMessage); + linkToken.transferFrom(_sender, address(this), fees); + + messageId = router.ccipSend(destinationChainSelector, evm2AnyMessage); + emit TokensTransferred( + messageId, + destinationChainSelector, + _sender, + receiver, + amountToTransfer, + address(linkToken), + fees + ); + + return messageId; + } + + function transferTokensPayNative( + uint64 _destinationChainSelector, + address _receiver, + uint256 _amount + ) external payable onlyOwner returns (bytes32 messageId) { + token.transferFrom(msg.sender, address(this), _amount); + + uint256 preWrapBalance = wrappedToken.balanceOf(address(this)); + wrappedToken.wrap(_amount); + uint256 amountToTransfer = wrappedToken.balanceOf(address(this)) - preWrapBalance; + + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(_receiver, amountToTransfer, address(0)); + + IRouterClient router = IRouterClient(this.getRouter()); + + uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage); + if (fees > msg.value) revert InsufficientFee(); + + messageId = router.ccipSend{value: fees}(_destinationChainSelector, evm2AnyMessage); + + if (fees < msg.value) { + (bool success, ) = msg.sender.call{value: msg.value - fees}(""); + if (!success) revert TransferFailed(); + } + + emit TokensTransferred( + messageId, + _destinationChainSelector, + msg.sender, + _receiver, + amountToTransfer, + address(0), + fees + ); + return messageId; + } + + function getCurrentFee(uint64 _destinationChainSelector, bool _payNative) external view returns (uint256) { + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + address(this), + 1000 ether, + _payNative ? address(0) : address(linkToken) + ); + + return IRouterClient(this.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); + } + + function _buildCCIPMessage( + address _receiver, + uint256 _amount, + address _feeTokenAddress + ) internal view returns (Client.EVM2AnyMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({token: address(wrappedToken), amount: _amount}); + tokenAmounts[0] = tokenAmount; + + Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(_receiver), + data: "", + tokenAmounts: tokenAmounts, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0, strict: false})), + feeToken: _feeTokenAddress + }); + + return evm2AnyMessage; + } + + function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage) internal override { + address tokenAddress = any2EvmMessage.destTokenAmounts[0].token; + uint256 tokenAmount = any2EvmMessage.destTokenAmounts[0].amount; + address receiver = abi.decode(any2EvmMessage.data, (address)); + + if (tokenAddress == address(wrappedToken)) { + uint256 preUnwrapBalance = token.balanceOf(address(this)); + wrappedToken.unwrap(tokenAmount); + uint256 amountToTransfer = token.balanceOf(address(this)) - preUnwrapBalance; + token.transfer(receiver, amountToTransfer); + } + + emit TokensReceived( + any2EvmMessage.messageId, + any2EvmMessage.sourceChainSelector, + abi.decode(any2EvmMessage.sender, (address)), + receiver, + tokenAmount + ); + } +} diff --git a/contracts/core/interfaces/IWrappedSDToken.sol b/contracts/core/interfaces/IWrappedLST.sol similarity index 94% rename from contracts/core/interfaces/IWrappedSDToken.sol rename to contracts/core/interfaces/IWrappedLST.sol index 2b27ba4c..4bfc0689 100644 --- a/contracts/core/interfaces/IWrappedSDToken.sol +++ b/contracts/core/interfaces/IWrappedLST.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.15; import "./IERC677.sol"; -interface IWrappedSDToken is IERC677 { +interface IWrappedLST is IERC677 { /** * @notice wraps tokens * @param _amount amount of unwrapped tokens to wrap From 2b669006de3787f21bd189c86e0dc8b1559ca410 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Tue, 17 Oct 2023 12:31:12 -0400 Subject: [PATCH 03/42] sepolia deployments --- .openzeppelin/sepolia.json | 1038 +++++++++++++++++++++++++++++++++ deployments/sepolia.json | 34 ++ scripts/test/setup-testnet.ts | 136 +++++ 3 files changed, 1208 insertions(+) create mode 100644 .openzeppelin/sepolia.json create mode 100644 deployments/sepolia.json create mode 100644 scripts/test/setup-testnet.ts diff --git a/.openzeppelin/sepolia.json b/.openzeppelin/sepolia.json new file mode 100644 index 00000000..f4cc4ee2 --- /dev/null +++ b/.openzeppelin/sepolia.json @@ -0,0 +1,1038 @@ +{ + "manifestVersion": "3.2", + "proxies": [ + { + "address": "0x31Fa516c6A602A1f7Fc4Ed0070Ee7Aea397cc4E7", + "txHash": "0xadd0ae9501565e2b673978bd267d9ffafd42716734354c246f59efefac9ef41a", + "kind": "uups" + }, + { + "address": "0x71EC4a95e3C26280d7FFc52c4BfEc538325b676b", + "txHash": "0xea35b1be8d680d552dd89eba5a539ccaaade526f1a17053f8c1af884303cb706", + "kind": "uups" + }, + { + "address": "0x6171927d7d982513af122C4Bc1C9d9E873e62273", + "txHash": "0x8b13617bb0784a1d0e44d9b20cec380d81c9f370d88e939ddb07849fcfce1116", + "kind": "uups" + }, + { + "address": "0x1bE3E37d9C219E4295402fFD855D0c80aF92E204", + "txHash": "0x5690899b7a4aa22982854fb7d0c9f510a1e97c36725afe77b105a44c75460c41", + "kind": "uups" + } + ], + "impls": { + "aaa26fdbac43af4ff59e4da0aaac9b93287eefbd3fd78f136d59dc7db3bc138d": { + "address": "0x8B84142DE59f7350C89f674248787c5EF9Dfcc91", + "txHash": "0x1f7ca64abebd678fe99180def0edad091716de29cd34ee8a5110020f49df6a72", + "layout": { + "solcVersion": "0.8.15", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "151", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "tokenPools", + "offset": 0, + "slot": "201", + "type": "t_mapping(t_address,t_contract(IRewardsPool)17161)", + "contract": "RewardsPoolController", + "src": "contracts/core/base/RewardsPoolController.sol:18" + }, + { + "label": "tokens", + "offset": 0, + "slot": "202", + "type": "t_array(t_address)dyn_storage", + "contract": "RewardsPoolController", + "src": "contracts/core/base/RewardsPoolController.sol:19" + }, + { + "label": "name", + "offset": 0, + "slot": "203", + "type": "t_string_storage", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:26" + }, + { + "label": "symbol", + "offset": 0, + "slot": "204", + "type": "t_string_storage", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:27" + }, + { + "label": "operatorApprovals", + "offset": 0, + "slot": "205", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:29" + }, + { + "label": "tokenApprovals", + "offset": 0, + "slot": "206", + "type": "t_mapping(t_uint256,t_address)", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:30" + }, + { + "label": "sdlToken", + "offset": 0, + "slot": "207", + "type": "t_contract(IERC20Upgradeable)3860", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:32" + }, + { + "label": "boostController", + "offset": 0, + "slot": "208", + "type": "t_contract(IBoostController)17066", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:33" + }, + { + "label": "lastLockId", + "offset": 0, + "slot": "209", + "type": "t_uint256", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:35" + }, + { + "label": "locks", + "offset": 0, + "slot": "210", + "type": "t_mapping(t_uint256,t_struct(Lock)19558_storage)", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:36" + }, + { + "label": "lockOwners", + "offset": 0, + "slot": "211", + "type": "t_mapping(t_uint256,t_address)", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:37" + }, + { + "label": "balances", + "offset": 0, + "slot": "212", + "type": "t_mapping(t_address,t_uint256)", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:38" + }, + { + "label": "totalEffectiveBalance", + "offset": 0, + "slot": "213", + "type": "t_uint256", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:40" + }, + { + "label": "effectiveBalances", + "offset": 0, + "slot": "214", + "type": "t_mapping(t_address,t_uint256)", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:41" + }, + { + "label": "delegatorPool", + "offset": 0, + "slot": "215", + "type": "t_address", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:43" + }, + { + "label": "baseURI", + "offset": 0, + "slot": "216", + "type": "t_string_storage", + "contract": "SDLPool", + "src": "contracts/core/sdlPool/SDLPool.sol:45" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IBoostController)17066": { + "label": "contract IBoostController", + "numberOfBytes": "20" + }, + "t_contract(IERC20Upgradeable)3860": { + "label": "contract IERC20Upgradeable", + "numberOfBytes": "20" + }, + "t_contract(IRewardsPool)17161": { + "label": "contract IRewardsPool", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_contract(IRewardsPool)17161)": { + "label": "mapping(address => contract IRewardsPool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_address)": { + "label": "mapping(uint256 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Lock)19558_storage)": { + "label": "mapping(uint256 => struct SDLPool.Lock)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Lock)19558_storage": { + "label": "struct SDLPool.Lock", + "members": [ + { + "label": "amount", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "boostAmount", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "startTime", + "type": "t_uint64", + "offset": 0, + "slot": "2" + }, + { + "label": "duration", + "type": "t_uint64", + "offset": 8, + "slot": "2" + }, + { + "label": "expiry", + "type": "t_uint64", + "offset": 16, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "5864ffebb3d88b5f8a1ce7ec87f09e1d4c6a192d97c8a6c7ac36bb17941fa816": { + "address": "0x3595Ca08a96D0a5f0B412A24412Eaa3758cFA81a", + "txHash": "0x8802644b2569d41c24d22d21345642b86640ab6e1e1c4ae9b99f147d90262e1a", + "layout": { + "solcVersion": "0.8.15", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_balances", + "offset": 0, + "slot": "51", + "type": "t_mapping(t_address,t_uint256)", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:40" + }, + { + "label": "_allowances", + "offset": 0, + "slot": "52", + "type": "t_mapping(t_address,t_mapping(t_address,t_uint256))", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:42" + }, + { + "label": "_totalSupply", + "offset": 0, + "slot": "53", + "type": "t_uint256", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:44" + }, + { + "label": "_name", + "offset": 0, + "slot": "54", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:46" + }, + { + "label": "_symbol", + "offset": 0, + "slot": "55", + "type": "t_string_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:47" + }, + { + "label": "__gap", + "offset": 0, + "slot": "56", + "type": "t_array(t_uint256)45_storage", + "contract": "ERC20Upgradeable", + "src": "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol:376" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" + }, + { + "label": "__gap", + "offset": 0, + "slot": "151", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" + }, + { + "label": "_owner", + "offset": 0, + "slot": "201", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "token", + "offset": 0, + "slot": "251", + "type": "t_contract(IERC20Upgradeable)3860", + "contract": "StakingRewardsPool", + "src": "contracts/core/base/StakingRewardsPool.sol:15" + }, + { + "label": "shares", + "offset": 0, + "slot": "252", + "type": "t_mapping(t_address,t_uint256)", + "contract": "StakingRewardsPool", + "src": "contracts/core/base/StakingRewardsPool.sol:17" + }, + { + "label": "totalShares", + "offset": 0, + "slot": "253", + "type": "t_uint256", + "contract": "StakingRewardsPool", + "src": "contracts/core/base/StakingRewardsPool.sol:18" + }, + { + "label": "strategies", + "offset": 0, + "slot": "254", + "type": "t_array(t_address)dyn_storage", + "contract": "StakingPool", + "src": "contracts/core/StakingPool.sol:22" + }, + { + "label": "totalStaked", + "offset": 0, + "slot": "255", + "type": "t_uint256", + "contract": "StakingPool", + "src": "contracts/core/StakingPool.sol:23" + }, + { + "label": "liquidityBuffer", + "offset": 0, + "slot": "256", + "type": "t_uint256", + "contract": "StakingPool", + "src": "contracts/core/StakingPool.sol:24" + }, + { + "label": "fees", + "offset": 0, + "slot": "257", + "type": "t_array(t_struct(Fee)14356_storage)dyn_storage", + "contract": "StakingPool", + "src": "contracts/core/StakingPool.sol:26" + }, + { + "label": "priorityPool", + "offset": 0, + "slot": "258", + "type": "t_address", + "contract": "StakingPool", + "src": "contracts/core/StakingPool.sol:28" + }, + { + "label": "delegatorPool", + "offset": 0, + "slot": "259", + "type": "t_address", + "contract": "StakingPool", + "src": "contracts/core/StakingPool.sol:29" + }, + { + "label": "poolIndex", + "offset": 20, + "slot": "259", + "type": "t_uint16", + "contract": "StakingPool", + "src": "contracts/core/StakingPool.sol:30" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_struct(Fee)14356_storage)dyn_storage": { + "label": "struct StakingPool.Fee[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)45_storage": { + "label": "uint256[45]", + "numberOfBytes": "1440" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IERC20Upgradeable)3860": { + "label": "contract IERC20Upgradeable", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_mapping(t_address,t_uint256))": { + "label": "mapping(address => mapping(address => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Fee)14356_storage": { + "label": "struct StakingPool.Fee", + "members": [ + { + "label": "receiver", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "basisPoints", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "df1ffc35bd7bcfcfbedb5b11b34df75a72c97cfaedd33fddc2daa97b67b9cf24": { + "address": "0xF4992e1e06280D375711CeB17ed4172F113913d8", + "txHash": "0x202819db64dae9827d4abba7d777a8688ae174940b86454d42c115ad229f3d1a", + "layout": { + "solcVersion": "0.8.15", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "151", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "_paused", + "offset": 0, + "slot": "201", + "type": "t_bool", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:29" + }, + { + "label": "__gap", + "offset": 0, + "slot": "202", + "type": "t_array(t_uint256)49_storage", + "contract": "PausableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol:116" + }, + { + "label": "token", + "offset": 0, + "slot": "251", + "type": "t_contract(IERC20Upgradeable)3860", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:29" + }, + { + "label": "stakingPool", + "offset": 0, + "slot": "252", + "type": "t_contract(IStakingPool)17324", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:30" + }, + { + "label": "sdlPool", + "offset": 0, + "slot": "253", + "type": "t_contract(ISDLPool)17196", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:31" + }, + { + "label": "distributionOracle", + "offset": 0, + "slot": "254", + "type": "t_address", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:32" + }, + { + "label": "queueDepositMin", + "offset": 0, + "slot": "255", + "type": "t_uint128", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:34" + }, + { + "label": "queueDepositMax", + "offset": 16, + "slot": "255", + "type": "t_uint128", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:35" + }, + { + "label": "poolStatus", + "offset": 0, + "slot": "256", + "type": "t_enum(PoolStatus)17970", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:36" + }, + { + "label": "merkleRoot", + "offset": 0, + "slot": "257", + "type": "t_bytes32", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:38" + }, + { + "label": "ipfsHash", + "offset": 0, + "slot": "258", + "type": "t_bytes32", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:39" + }, + { + "label": "merkleTreeSize", + "offset": 0, + "slot": "259", + "type": "t_uint256", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:40" + }, + { + "label": "totalQueued", + "offset": 0, + "slot": "260", + "type": "t_uint256", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:42" + }, + { + "label": "depositsSinceLastUpdate", + "offset": 0, + "slot": "261", + "type": "t_uint256", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:43" + }, + { + "label": "sharesSinceLastUpdate", + "offset": 0, + "slot": "262", + "type": "t_uint256", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:44" + }, + { + "label": "accounts", + "offset": 0, + "slot": "263", + "type": "t_array(t_address)dyn_storage", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:46" + }, + { + "label": "accountIndexes", + "offset": 0, + "slot": "264", + "type": "t_mapping(t_address,t_uint256)", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:47" + }, + { + "label": "accountQueuedTokens", + "offset": 0, + "slot": "265", + "type": "t_mapping(t_address,t_uint256)", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:48" + }, + { + "label": "accountClaimed", + "offset": 0, + "slot": "266", + "type": "t_mapping(t_address,t_uint256)", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:49" + }, + { + "label": "accountSharesClaimed", + "offset": 0, + "slot": "267", + "type": "t_mapping(t_address,t_uint256)", + "contract": "PriorityPool", + "src": "contracts/core/priorityPool/PriorityPool.sol:50" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(IERC20Upgradeable)3860": { + "label": "contract IERC20Upgradeable", + "numberOfBytes": "20" + }, + "t_contract(ISDLPool)17196": { + "label": "contract ISDLPool", + "numberOfBytes": "20" + }, + "t_contract(IStakingPool)17324": { + "label": "contract IStakingPool", + "numberOfBytes": "20" + }, + "t_enum(PoolStatus)17970": { + "label": "enum PriorityPool.PoolStatus", + "members": [ + "OPEN", + "DRAINING", + "CLOSED" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_uint128": { + "label": "uint128", + "numberOfBytes": "16" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + }, + "00549b222c9c3bf8b8c12041f68ba4cefb9515cda1fbae778a3b3904d8685cfb": { + "address": "0x5f56ECd0BAd3ED8A2f675AF947F1b2793AA7bE02", + "txHash": "0x1e50eae0b7b3209c1c4020ce60e4e4a509d6b15cef830e8f0eb1dc148f1dac0a", + "layout": { + "solcVersion": "0.8.15", + "storage": [ + { + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "t_uint8", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63", + "retypedFrom": "bool" + }, + { + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "t_bool", + "contract": "Initializable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68" + }, + { + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "t_array(t_uint256)50_storage", + "contract": "ERC1967UpgradeUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169" + }, + { + "label": "__gap", + "offset": 0, + "slot": "51", + "type": "t_array(t_uint256)50_storage", + "contract": "UUPSUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111" + }, + { + "label": "__gap", + "offset": 0, + "slot": "101", + "type": "t_array(t_uint256)50_storage", + "contract": "ContextUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:36" + }, + { + "label": "_owner", + "offset": 0, + "slot": "151", + "type": "t_address", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22" + }, + { + "label": "__gap", + "offset": 0, + "slot": "152", + "type": "t_array(t_uint256)49_storage", + "contract": "OwnableUpgradeable", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94" + }, + { + "label": "token", + "offset": 0, + "slot": "201", + "type": "t_contract(IERC20Upgradeable)3860", + "contract": "Strategy", + "src": "contracts/core/base/Strategy.sol:17" + }, + { + "label": "stakingPool", + "offset": 0, + "slot": "202", + "type": "t_contract(IStakingPool)17324", + "contract": "Strategy", + "src": "contracts/core/base/Strategy.sol:18" + }, + { + "label": "maxDeposits", + "offset": 0, + "slot": "203", + "type": "t_uint256", + "contract": "StrategyMock", + "src": "contracts/core/test/StrategyMock.sol:17" + }, + { + "label": "minDeposits", + "offset": 0, + "slot": "204", + "type": "t_uint256", + "contract": "StrategyMock", + "src": "contracts/core/test/StrategyMock.sol:18" + }, + { + "label": "totalDeposits", + "offset": 0, + "slot": "205", + "type": "t_uint256", + "contract": "StrategyMock", + "src": "contracts/core/test/StrategyMock.sol:20" + }, + { + "label": "feeBasisPoints", + "offset": 0, + "slot": "206", + "type": "t_uint256", + "contract": "StrategyMock", + "src": "contracts/core/test/StrategyMock.sol:21" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_uint256)49_storage": { + "label": "uint256[49]", + "numberOfBytes": "1568" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_contract(IERC20Upgradeable)3860": { + "label": "contract IERC20Upgradeable", + "numberOfBytes": "20" + }, + "t_contract(IStakingPool)17324": { + "label": "contract IStakingPool", + "numberOfBytes": "20" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + } + } + } + } +} diff --git a/deployments/sepolia.json b/deployments/sepolia.json new file mode 100644 index 00000000..68a8f6ee --- /dev/null +++ b/deployments/sepolia.json @@ -0,0 +1,34 @@ +{ + "SDLToken": { + "address": "0xD668b598e7F9d72b0D1B081c928eC8553503a188", + "artifact": "StakingAllowance" + }, + "LinearBoostController": { + "address": "0xCaEC3A1abD45Df880fD1B3252bdF29e5c4EE9eB7", + "artifact": "LinearBoostController" + }, + "SDLPool": { + "address": "0x31Fa516c6A602A1f7Fc4Ed0070Ee7Aea397cc4E7", + "artifact": "SDLPool" + }, + "LINKToken": { + "address": "0x34e754e54b286250fea278AeC99033Ae9e509e25", + "artifact": "ERC677" + }, + "LINK_StakingPool": { + "address": "0x71EC4a95e3C26280d7FFc52c4BfEc538325b676b", + "artifact": "StakingPool" + }, + "LINK_PriorityPool": { + "address": "0x6171927d7d982513af122C4Bc1C9d9E873e62273", + "artifact": "PriorityPool" + }, + "LINK_WrappedSDToken": { + "address": "0xE7a999ae5670B5b2B725A55d4b0E686B9e190826", + "artifact": "WrappedSDToken" + }, + "stLINK_SDLRewardsPool": { + "address": "0x0415A4374d1f16B79b3604DE13D0f15e58C0539d", + "artifact": "RewardsPoolWSD" + } +} diff --git a/scripts/test/setup-testnet.ts b/scripts/test/setup-testnet.ts new file mode 100644 index 00000000..5e3a6647 --- /dev/null +++ b/scripts/test/setup-testnet.ts @@ -0,0 +1,136 @@ +import { updateDeployments, deploy, deployUpgradeable } from '../utils/deployment' +import { getAccounts, toEther } from '../utils/helpers' + +// SDL Token +const StakingAllowance = { + name: 'stake.link', // SDL token name + symbol: 'SDL', // SDL token symbol +} +// Linear Boost Controller +const LinearBoostController = { + maxLockingDuration: 4 * 365 * 86400, // maximum locking duration + maxBoost: 8, // maximum boost amount +} +// SDL Pool +const SDLPool = { + derivativeTokenName: 'Reward Escrowed SDL', // SDL staking derivative token name + derivativeTokenSymbol: 'reSDL', // SDL staking derivative token symbol +} +// LINK Staking Pool +const LINK_StakingPool = { + derivativeTokenName: 'Staked LINK', // LINK staking derivative token name + derivativeTokenSymbol: 'stLINK', // LINK staking derivative token symbol + fees: [['0x6879826450e576B401c4dDeff2B7755B1e85d97c', 300]], // fee receivers & percentage amounts in basis points +} +// LINK Priority Pool +const LINK_PriorityPool = { + queueDepositMin: toEther(1000), // min amount of tokens neede to execute deposit + queueDepositMax: toEther(200000), // max amount of tokens in a single deposit tx +} +// LINK Wrapped Staking Derivative Token +const LINK_WrappedSDToken = { + name: 'Wrapped stLINK', // wrapped staking derivative token name + symbol: 'wstLINK', // wrapped staking derivative token symbol +} + +async function main() { + const { accounts } = await getAccounts() + + const sdlToken = await deploy('StakingAllowance', [ + StakingAllowance.name, + StakingAllowance.symbol, + ]) + console.log('SDLToken deployed: ', sdlToken.address) + + await (await sdlToken.mint(accounts[0], toEther(100000000))).wait() + + const lbc = await deploy('LinearBoostController', [ + LinearBoostController.maxLockingDuration, + LinearBoostController.maxBoost, + ]) + console.log('LinearBoostController deployed: ', lbc.address) + + const sdlPool = await deployUpgradeable('SDLPool', [ + SDLPool.derivativeTokenName, + SDLPool.derivativeTokenSymbol, + sdlToken.address, + lbc.address, + accounts[0], + ]) + console.log('SDLPool deployed: ', sdlPool.address) + + const linkToken = await deploy('ERC677', ['Chainlink-Test', 'LINK-TEST', 200000000]) + console.log('LINKToken-TEST deployed: ', linkToken.address) + + const stakingPool = await deployUpgradeable('StakingPool', [ + linkToken.address, + LINK_StakingPool.derivativeTokenName, + LINK_StakingPool.derivativeTokenSymbol, + LINK_StakingPool.fees, + ]) + console.log('LINK_StakingPool deployed: ', stakingPool.address) + + const priorityPool = await deployUpgradeable('PriorityPool', [ + linkToken.address, + stakingPool.address, + sdlPool.address, + LINK_PriorityPool.queueDepositMin, + LINK_PriorityPool.queueDepositMax, + ]) + console.log('LINK_PriorityPool deployed: ', priorityPool.address) + + await (await stakingPool.setPriorityPool(priorityPool.address)).wait() + + const strategy = await deployUpgradeable('StrategyMock', [ + linkToken.address, + stakingPool.address, + toEther(1000000), + toEther(1000000), + ]) + + await (await stakingPool.addStrategy(strategy.address)).wait() + + const wsdToken = await deploy('WrappedSDToken', [ + stakingPool.address, + LINK_WrappedSDToken.name, + LINK_WrappedSDToken.symbol, + ]) + console.log('LINK_WrappedSDToken token deployed: ', wsdToken.address) + + const stLinkSDLRewardsPool = await deploy('RewardsPoolWSD', [ + sdlPool.address, + stakingPool.address, + wsdToken.address, + ]) + console.log('stLINK_SDLRewardsPool deployed: ', stLinkSDLRewardsPool.address) + + await (await sdlPool.addToken(stakingPool.address, stLinkSDLRewardsPool.address)).wait() + + updateDeployments( + { + SDLToken: sdlToken.address, + LinearBoostController: lbc.address, + SDLPool: sdlPool.address, + LINKToken: linkToken.address, + LINK_StakingPool: stakingPool.address, + LINK_PriorityPool: priorityPool.address, + LINK_WrappedSDToken: wsdToken.address, + stLINK_SDLRewardsPool: stLinkSDLRewardsPool.address, + }, + { + SDLToken: 'StakingAllowance', + LINKToken: 'ERC677', + LINK_StakingPool: 'StakingPool', + LINK_PriorityPool: 'PriorityPool', + LINK_WrappedSDToken: 'WrappedSDToken', + stLINK_SDLRewardsPool: 'RewardsPoolWSD', + } + ) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) From 93f2a6d42af05e317543e6dc0e15095bf00c87e2 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Sun, 22 Oct 2023 15:55:28 -0400 Subject: [PATCH 04/42] handle invalid messages --- contracts/core/ccip/WrappedTokenBridge.sol | 179 ++++++++++++++------- 1 file changed, 123 insertions(+), 56 deletions(-) diff --git a/contracts/core/ccip/WrappedTokenBridge.sol b/contracts/core/ccip/WrappedTokenBridge.sol index 97767388..aa448870 100644 --- a/contracts/core/ccip/WrappedTokenBridge.sol +++ b/contracts/core/ccip/WrappedTokenBridge.sol @@ -3,18 +3,28 @@ pragma solidity 0.8.15; import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol"; import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; -import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol"; import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../interfaces/IWrappedLST.sol"; contract WrappedTokenBridge is Ownable, CCIPReceiver { - LinkTokenInterface linkToken; + using SafeERC20 for IERC20; + + enum ErrorStatus { + RESOLVED, + UNRESOLVED + } + + IERC20 linkToken; IERC20 token; IWrappedLST wrappedToken; + mapping(bytes32 => ErrorStatus) public messageErrorsStatus; + mapping(bytes32 => Client.Any2EVMMessage) public failedMessages; + event TokensTransferred( bytes32 indexed messageId, uint64 indexed destinationChainSelector, @@ -31,11 +41,22 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { address receiver, uint256 tokenAmount ); + event MessageFailed(bytes32 indexed messageId, bytes error); + event MessageResolved(bytes32 indexed messageId); error InvalidSender(); error InvalidValue(); error InsufficientFee(); error TransferFailed(); + error FeeExceedsLimit(); + error OnlySelf(); + error MessageIsResolved(); + error InvalidMessage(); + + modifier onlySelf() { + if (msg.sender != address(this)) revert OnlySelf(); + _; + } constructor( address _router, @@ -43,7 +64,7 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { address _token, address _wrappedToken ) CCIPReceiver(_router) { - linkToken = LinkTokenInterface(_linkToken); + linkToken = IERC20(_linkToken); token = IERC20(_token); wrappedToken = IWrappedLST(_wrappedToken); @@ -57,87 +78,131 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { address _sender, uint256 _value, bytes calldata _calldata - ) external returns (bytes32 messageId) { + ) external { if (msg.sender != address(token)) revert InvalidSender(); if (_value == 0) revert InvalidValue(); - uint256 preWrapBalance = wrappedToken.balanceOf(address(this)); - wrappedToken.wrap(_value); - uint256 amountToTransfer = wrappedToken.balanceOf(address(this)) - preWrapBalance; + (uint64 destinationChainSelector, address receiver, uint256 maxLINKFee, bytes memory extraArgs) = abi.decode( + _calldata, + (uint64, address, uint256, bytes) + ); + _transferTokens(destinationChainSelector, _sender, receiver, _value, false, maxLINKFee, extraArgs); + } - (uint64 destinationChainSelector, address receiver) = abi.decode(_calldata, (uint64, address)); - Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(receiver, amountToTransfer, address(linkToken)); + function transferTokens( + uint64 _destinationChainSelector, + address _receiver, + uint256 _amount, + bool _payNative, + uint256 _maxLINKFee, + bytes memory _extraArgs + ) external payable onlyOwner returns (bytes32 messageId) { + token.safeTransferFrom(msg.sender, address(this), _amount); + return + _transferTokens(_destinationChainSelector, msg.sender, _receiver, _amount, _payNative, _maxLINKFee, _extraArgs); + } - IRouterClient router = IRouterClient(this.getRouter()); + function getFee( + uint64 _destinationChainSelector, + bool _payNative, + bytes memory _extraArgs + ) external view returns (uint256) { + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + address(this), + 1000 ether, + _payNative ? address(0) : address(linkToken), + _extraArgs + ); - uint256 fees = router.getFee(destinationChainSelector, evm2AnyMessage); - linkToken.transferFrom(_sender, address(this), fees); + return IRouterClient(this.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); + } - messageId = router.ccipSend(destinationChainSelector, evm2AnyMessage); - emit TokensTransferred( - messageId, - destinationChainSelector, - _sender, - receiver, - amountToTransfer, - address(linkToken), - fees - ); + function ccipReceive(Client.Any2EVMMessage calldata _any2EvmMessage) external override onlyRouter { + try this.processMessage(_any2EvmMessage) {} catch (bytes memory err) { + bytes32 messageId = _any2EvmMessage.messageId; + messageErrorsStatus[messageId] = ErrorStatus.UNRESOLVED; + failedMessages[messageId] = _any2EvmMessage; + emit MessageFailed(messageId, err); + } + } - return messageId; + function processMessage(Client.Any2EVMMessage calldata _any2EvmMessage) external onlySelf { + _ccipReceive(_any2EvmMessage); } - function transferTokensPayNative( + function retryFailedMessage(bytes32 _messageId, address tokenReceiver) external onlyOwner { + if (messageErrorsStatus[_messageId] != ErrorStatus.UNRESOLVED) revert MessageIsResolved(); + + messageErrorsStatus[_messageId] = ErrorStatus.RESOLVED; + + Client.Any2EVMMessage memory message = failedMessages[_messageId]; + for (uint256 i = 0; i < message.destTokenAmounts.length; ++i) { + IERC20(message.destTokenAmounts[i].token).safeTransfer(tokenReceiver, message.destTokenAmounts[i].amount); + } + + emit MessageResolved(_messageId); + } + + function recoverTokens(address[] calldata _tokens, address _receiver) external onlyOwner { + for (uint256 i = 0; i < _tokens.length; ++i) { + IERC20 tokenToTransfer = IERC20(_tokens[i]); + tokenToTransfer.safeTransfer(_receiver, tokenToTransfer.balanceOf(address(this))); + } + } + + function _transferTokens( uint64 _destinationChainSelector, + address _sender, address _receiver, - uint256 _amount - ) external payable onlyOwner returns (bytes32 messageId) { - token.transferFrom(msg.sender, address(this), _amount); - + uint256 _amount, + bool _payNative, + uint256 _maxLINKFee, + bytes memory _extraArgs + ) internal returns (bytes32 messageId) { uint256 preWrapBalance = wrappedToken.balanceOf(address(this)); wrappedToken.wrap(_amount); uint256 amountToTransfer = wrappedToken.balanceOf(address(this)) - preWrapBalance; - Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(_receiver, amountToTransfer, address(0)); + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + _receiver, + amountToTransfer, + _payNative ? address(0) : address(linkToken), + _extraArgs + ); IRouterClient router = IRouterClient(this.getRouter()); - uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage); - if (fees > msg.value) revert InsufficientFee(); - messageId = router.ccipSend{value: fees}(_destinationChainSelector, evm2AnyMessage); - - if (fees < msg.value) { - (bool success, ) = msg.sender.call{value: msg.value - fees}(""); - if (!success) revert TransferFailed(); + if (_payNative) { + if (fees > msg.value) revert InsufficientFee(); + messageId = router.ccipSend{value: fees}(_destinationChainSelector, evm2AnyMessage); + if (fees < msg.value) { + (bool success, ) = _sender.call{value: msg.value - fees}(""); + if (!success) revert TransferFailed(); + } + } else { + if (fees > _maxLINKFee) revert FeeExceedsLimit(); + linkToken.safeTransferFrom(_sender, address(this), fees); + messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage); } emit TokensTransferred( messageId, _destinationChainSelector, - msg.sender, + _sender, _receiver, amountToTransfer, - address(0), + _payNative ? address(0) : address(linkToken), fees ); return messageId; } - function getCurrentFee(uint64 _destinationChainSelector, bool _payNative) external view returns (uint256) { - Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( - address(this), - 1000 ether, - _payNative ? address(0) : address(linkToken) - ); - - return IRouterClient(this.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); - } - function _buildCCIPMessage( address _receiver, uint256 _amount, - address _feeTokenAddress + address _feeTokenAddress, + bytes memory _extraArgs ) internal view returns (Client.EVM2AnyMessage memory) { Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({token: address(wrappedToken), amount: _amount}); @@ -147,7 +212,7 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { receiver: abi.encode(_receiver), data: "", tokenAmounts: tokenAmounts, - extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0, strict: false})), + extraArgs: _extraArgs, feeToken: _feeTokenAddress }); @@ -155,16 +220,18 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { } function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage) internal override { + if (any2EvmMessage.destTokenAmounts.length != 1) revert InvalidMessage(); + address tokenAddress = any2EvmMessage.destTokenAmounts[0].token; uint256 tokenAmount = any2EvmMessage.destTokenAmounts[0].amount; address receiver = abi.decode(any2EvmMessage.data, (address)); - if (tokenAddress == address(wrappedToken)) { - uint256 preUnwrapBalance = token.balanceOf(address(this)); - wrappedToken.unwrap(tokenAmount); - uint256 amountToTransfer = token.balanceOf(address(this)) - preUnwrapBalance; - token.transfer(receiver, amountToTransfer); - } + if (tokenAddress != address(wrappedToken) || receiver == address(0)) revert InvalidMessage(); + + uint256 preUnwrapBalance = token.balanceOf(address(this)); + wrappedToken.unwrap(tokenAmount); + uint256 amountToTransfer = token.balanceOf(address(this)) - preUnwrapBalance; + token.safeTransfer(receiver, amountToTransfer); emit TokensReceived( any2EvmMessage.messageId, From dcf2a76c5c9a7e83ca2586da211286cbddeaf6fe Mon Sep 17 00:00:00 2001 From: BkChoy Date: Sun, 22 Oct 2023 15:55:53 -0400 Subject: [PATCH 05/42] wrapped token bridge tests --- .../core/test/chainlink/CCIPArmProxyMock.sol | 12 + .../core/test/chainlink/CCIPOffRampMock.sol | 52 ++++ .../core/test/chainlink/CCIPOnRampMock.sol | 54 ++++ .../core/test/chainlink/CCIPTokenPoolMock.sol | 26 ++ .../CLContractImports0.7.sol} | 0 .../test/chainlink/CLContractImports0.8.sol | 4 + .../core/test/chainlink/WrappedNative.sol | 12 + test/core/ccip/wrapped-token-bridge.test.ts | 290 ++++++++++++++++++ 8 files changed, 450 insertions(+) create mode 100644 contracts/core/test/chainlink/CCIPArmProxyMock.sol create mode 100644 contracts/core/test/chainlink/CCIPOffRampMock.sol create mode 100644 contracts/core/test/chainlink/CCIPOnRampMock.sol create mode 100644 contracts/core/test/chainlink/CCIPTokenPoolMock.sol rename contracts/core/test/{CLContractImports.sol => chainlink/CLContractImports0.7.sol} (100%) create mode 100644 contracts/core/test/chainlink/CLContractImports0.8.sol create mode 100644 contracts/core/test/chainlink/WrappedNative.sol create mode 100644 test/core/ccip/wrapped-token-bridge.test.ts diff --git a/contracts/core/test/chainlink/CCIPArmProxyMock.sol b/contracts/core/test/chainlink/CCIPArmProxyMock.sol new file mode 100644 index 00000000..e2605cf7 --- /dev/null +++ b/contracts/core/test/chainlink/CCIPArmProxyMock.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.15; + +/** + * @title CCIP ARMProxy Mock + * @notice Mocks CCIP contract for testing + */ +contract CCIPArmProxyMock { + function isCursed() external returns (bool) { + return false; + } +} diff --git a/contracts/core/test/chainlink/CCIPOffRampMock.sol b/contracts/core/test/chainlink/CCIPOffRampMock.sol new file mode 100644 index 00000000..f08d90c6 --- /dev/null +++ b/contracts/core/test/chainlink/CCIPOffRampMock.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.15; + +import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; +import {IRouter} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouter.sol"; + +interface ITokenPool { + function token() external view returns (address); + + function releaseOrMint(address _receiver, uint256 _amount) external; +} + +/** + * @title CCIP OffRamp Mock + * @notice Mocks CCIP offramp contract for testing + */ +contract CCIPOffRampMock { + uint16 private constant GAS_FOR_CALL_EXACT_CHECK = 5_000; + + IRouter public router; + mapping(address => ITokenPool) public tokenPools; + + constructor( + address _router, + address[] memory _tokens, + address[] memory _tokenPools + ) { + router = IRouter(_router); + for (uint256 i = 0; i < _tokens.length; ++i) { + tokenPools[_tokens[i]] = ITokenPool(_tokenPools[i]); + } + } + + function executeSingleMessage( + bytes32 _messageId, + uint64 _sourceChainSelector, + bytes calldata _data, + address _receiver, + Client.EVMTokenAmount[] calldata _tokenAmounts + ) external { + for (uint256 i = 0; i < _tokenAmounts.length; ++i) { + tokenPools[_tokenAmounts[i].token].releaseOrMint(_receiver, _tokenAmounts[i].amount); + } + + router.routeMessage( + Client.Any2EVMMessage(_messageId, _sourceChainSelector, abi.encode(msg.sender), _data, _tokenAmounts), + GAS_FOR_CALL_EXACT_CHECK, + 1000000, + _receiver + ); + } +} diff --git a/contracts/core/test/chainlink/CCIPOnRampMock.sol b/contracts/core/test/chainlink/CCIPOnRampMock.sol new file mode 100644 index 00000000..7ba8668e --- /dev/null +++ b/contracts/core/test/chainlink/CCIPOnRampMock.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.15; + +import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; + +/** + * @title CCIP OnRamp Mock + * @notice Mocks CCIP onramp contract for testing + */ +contract CCIPOnRampMock { + struct LastRequestData { + uint256 feeTokenAmount; + address originalSender; + } + + mapping(address => address) public tokenPools; + address public linkToken; + + Client.EVM2AnyMessage private lastRequestMessage; + LastRequestData public lastRequestData; + + constructor( + address[] memory _tokens, + address[] memory _tokenPools, + address _linkToken + ) { + for (uint256 i = 0; i < _tokens.length; ++i) { + tokenPools[_tokens[i]] = _tokenPools[i]; + } + linkToken = _linkToken; + } + + function getLastRequestMessage() external view returns (Client.EVM2AnyMessage memory) { + return lastRequestMessage; + } + + function getFee(Client.EVM2AnyMessage calldata _message) external view returns (uint256) { + return _message.feeToken == linkToken ? 2 ether : 3 ether; + } + + function getPoolBySourceToken(address _token) public view returns (address) { + return tokenPools[_token]; + } + + function forwardFromRouter( + Client.EVM2AnyMessage calldata _message, + uint256 _feeTokenAmount, + address _originalSender + ) external returns (bytes32) { + lastRequestMessage = _message; + lastRequestData = LastRequestData(_feeTokenAmount, _originalSender); + return keccak256(abi.encode(block.timestamp)); + } +} diff --git a/contracts/core/test/chainlink/CCIPTokenPoolMock.sol b/contracts/core/test/chainlink/CCIPTokenPoolMock.sol new file mode 100644 index 00000000..be1dc641 --- /dev/null +++ b/contracts/core/test/chainlink/CCIPTokenPoolMock.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.15; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; + +/** + * @title CCIP Token Pool Mock + * @notice Mocks CCIP token pool contract for testing + */ +contract CCIPTokenPoolMock { + using SafeERC20 for IERC20; + + IERC20 public token; + + constructor(address _token) { + token = IERC20(_token); + } + + function lockOrBurn() external {} + + function releaseOrMint(address _receiver, uint256 _amount) external { + token.safeTransfer(_receiver, _amount); + } +} diff --git a/contracts/core/test/CLContractImports.sol b/contracts/core/test/chainlink/CLContractImports0.7.sol similarity index 100% rename from contracts/core/test/CLContractImports.sol rename to contracts/core/test/chainlink/CLContractImports0.7.sol diff --git a/contracts/core/test/chainlink/CLContractImports0.8.sol b/contracts/core/test/chainlink/CLContractImports0.8.sol new file mode 100644 index 00000000..6bc07448 --- /dev/null +++ b/contracts/core/test/chainlink/CLContractImports0.8.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +import {Router} from "@chainlink/contracts-ccip/src/v0.8/ccip/Router.sol"; diff --git a/contracts/core/test/chainlink/WrappedNative.sol b/contracts/core/test/chainlink/WrappedNative.sol new file mode 100644 index 00000000..5200c4ae --- /dev/null +++ b/contracts/core/test/chainlink/WrappedNative.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.15; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract WrappedNative is ERC20 { + constructor() ERC20("WrappedNative", "WN") {} + + function deposit() external payable { + _mint(msg.sender, msg.value); + } +} diff --git a/test/core/ccip/wrapped-token-bridge.test.ts b/test/core/ccip/wrapped-token-bridge.test.ts new file mode 100644 index 00000000..3d49fe99 --- /dev/null +++ b/test/core/ccip/wrapped-token-bridge.test.ts @@ -0,0 +1,290 @@ +import { ethers } from 'hardhat' +import { assert, expect } from 'chai' +import { toEther, deploy, deployUpgradeable, getAccounts, fromEther } from '../../utils/helpers' +import { + ERC677, + StrategyMock, + StakingPool, + WrappedSDToken, + WrappedTokenBridge, + CCIPOnRampMock, + CCIPOffRampMock, + CCIPTokenPoolMock, + WrappedNative, +} from '../../../typechain-types' + +describe('WrappedTokenBridge', () => { + let linkToken: ERC677 + let token2: ERC677 + let wrappedToken: WrappedSDToken + let stakingPool: StakingPool + let bridge: WrappedTokenBridge + let onRamp: CCIPOnRampMock + let offRamp: CCIPOffRampMock + let tokenPool: CCIPTokenPoolMock + let tokenPool2: CCIPTokenPoolMock + let wrappedNative: WrappedNative + let accounts: string[] + + before(async () => { + ;({ accounts } = await getAccounts()) + }) + + beforeEach(async () => { + linkToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + token2 = (await deploy('ERC677', ['2', '2', 1000000000])) as ERC677 + + stakingPool = (await deployUpgradeable('StakingPool', [ + linkToken.address, + 'Staked LINK', + 'stLINK', + [], + ])) as StakingPool + + wrappedToken = (await deploy('WrappedSDToken', [ + stakingPool.address, + 'Wrapped stLINK', + 'wstLINK', + ])) as WrappedSDToken + + const strategy = (await deployUpgradeable('StrategyMock', [ + linkToken.address, + stakingPool.address, + toEther(100000), + toEther(0), + ])) as StrategyMock + + await stakingPool.addStrategy(strategy.address) + await stakingPool.setPriorityPool(accounts[0]) + + await linkToken.approve(stakingPool.address, ethers.constants.MaxUint256) + await stakingPool.deposit(accounts[0], toEther(10000)) + await stakingPool.deposit(accounts[1], toEther(2000)) + await linkToken.transfer(strategy.address, toEther(12000)) + await stakingPool.updateStrategyRewards([0], '0x') + + wrappedNative = (await deploy('WrappedNative')) as WrappedNative + const armProxy = await deploy('CCIPArmProxyMock') + const router = await deploy('Router', [wrappedNative.address, armProxy.address]) + tokenPool = (await deploy('CCIPTokenPoolMock', [wrappedToken.address])) as CCIPTokenPoolMock + tokenPool2 = (await deploy('CCIPTokenPoolMock', [token2.address])) as CCIPTokenPoolMock + onRamp = (await deploy('CCIPOnRampMock', [ + [wrappedToken.address, token2.address], + [tokenPool.address, tokenPool2.address], + linkToken.address, + ])) as CCIPOnRampMock + offRamp = (await deploy('CCIPOffRampMock', [ + router.address, + [wrappedToken.address, token2.address], + [tokenPool.address, tokenPool2.address], + ])) as CCIPOffRampMock + + await router.applyRampUpdates([[77, onRamp.address]], [], [[77, offRamp.address]]) + + bridge = (await deploy('WrappedTokenBridge', [ + router.address, + linkToken.address, + stakingPool.address, + wrappedToken.address, + ])) as WrappedTokenBridge + + await linkToken.approve(bridge.address, ethers.constants.MaxUint256) + await stakingPool.approve(bridge.address, ethers.constants.MaxUint256) + }) + + it('getFee should work correctly', async () => { + assert.equal(fromEther(await bridge.getFee(77, false, '0x')), 2) + assert.equal(fromEther(await bridge.getFee(77, true, '0x')), 3) + await expect(bridge.getFee(78, false, '0x')).to.be.reverted + await expect(bridge.getFee(78, true, '0x')).to.be.reverted + }) + + it('transferTokens should work correctly with LINK fee', async () => { + let preFeeBalance = await linkToken.balanceOf(accounts[0]) + + await bridge.transferTokens(77, accounts[4], toEther(100), false, toEther(10), '0x') + let lastRequestData = await onRamp.lastRequestData() + let lastRequestMsg = await onRamp.getLastRequestMessage() + + assert.equal(fromEther(await wrappedToken.balanceOf(tokenPool.address)), 50) + assert.equal(fromEther(preFeeBalance.sub(await linkToken.balanceOf(accounts[0]))), 2) + + assert.equal(fromEther(lastRequestData[0]), 2) + assert.equal(lastRequestData[1], bridge.address) + + assert.equal( + ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], + accounts[4] + ) + assert.equal(lastRequestMsg[1], '0x') + assert.deepEqual( + lastRequestMsg[2].map((d) => [d.token, fromEther(d.amount)]), + [[wrappedToken.address, 50]] + ) + assert.equal(lastRequestMsg[3], linkToken.address) + + await expect( + bridge.transferTokens(77, accounts[4], toEther(100), false, toEther(1), '0x') + ).to.be.revertedWith('FeeExceedsLimit()') + }) + + it('transferTokens should work correctly with native fee', async () => { + let preFeeBalance = await ethers.provider.getBalance(accounts[0]) + + await bridge.transferTokens(77, accounts[4], toEther(100), true, 0, '0x', { + value: toEther(10), + }) + let lastRequestData = await onRamp.lastRequestData() + let lastRequestMsg = await onRamp.getLastRequestMessage() + + assert.equal(fromEther(await wrappedToken.balanceOf(tokenPool.address)), 50) + assert.equal( + Math.trunc(fromEther(preFeeBalance.sub(await ethers.provider.getBalance(accounts[0])))), + 3 + ) + + assert.equal(fromEther(lastRequestData[0]), 3) + assert.equal(lastRequestData[1], bridge.address) + + assert.equal( + ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], + accounts[4] + ) + assert.equal(lastRequestMsg[1], '0x') + assert.deepEqual( + lastRequestMsg[2].map((d) => [d.token, fromEther(d.amount)]), + [[wrappedToken.address, 50]] + ) + assert.equal(lastRequestMsg[3], wrappedNative.address) + }) + + it('onTokenTransfer should work correctly', async () => { + let preFeeBalance = await linkToken.balanceOf(accounts[0]) + + await stakingPool.transferAndCall( + bridge.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode( + ['uint64', 'address', 'uint256', 'bytes'], + [77, accounts[4], toEther(10), '0x'] + ) + ) + + let lastRequestData = await onRamp.lastRequestData() + let lastRequestMsg = await onRamp.getLastRequestMessage() + + assert.equal(fromEther(await wrappedToken.balanceOf(tokenPool.address)), 50) + assert.equal(fromEther(preFeeBalance.sub(await linkToken.balanceOf(accounts[0]))), 2) + + assert.equal(fromEther(lastRequestData[0]), 2) + assert.equal(lastRequestData[1], bridge.address) + + assert.equal( + ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], + accounts[4] + ) + assert.equal(lastRequestMsg[1], '0x') + assert.deepEqual( + lastRequestMsg[2].map((d) => [d.token, fromEther(d.amount)]), + [[wrappedToken.address, 50]] + ) + assert.equal(lastRequestMsg[3], linkToken.address) + + await expect(bridge.onTokenTransfer(accounts[0], toEther(1000), '0x')).to.be.revertedWith( + 'InvalidSender()' + ) + await expect(stakingPool.transferAndCall(bridge.address, 0, '0x')).to.be.revertedWith( + 'InvalidValue()' + ) + await expect( + stakingPool.transferAndCall( + bridge.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode( + ['uint64', 'address', 'uint256', 'bytes'], + [77, accounts[4], toEther(1), '0x'] + ) + ) + ).to.be.revertedWith('FeeExceedsLimit()') + }) + + it('ccipReceive should work correctly', async () => { + await stakingPool.transferAndCall( + bridge.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode( + ['uint64', 'address', 'uint256', 'bytes'], + [77, accounts[4], toEther(10), '0x'] + ) + ) + await offRamp.executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + ethers.utils.defaultAbiCoder.encode(['address'], [accounts[5]]), + bridge.address, + [{ token: wrappedToken.address, amount: toEther(25) }] + ) + + assert.equal(fromEther(await stakingPool.balanceOf(accounts[5])), 50) + }) + + it('failed messages should be properly handled', async () => { + await stakingPool.transferAndCall( + bridge.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode( + ['uint64', 'address', 'uint256', 'bytes'], + [77, accounts[4], toEther(10), '0x'] + ) + ) + await token2.transfer(tokenPool2.address, toEther(100)) + await offRamp.executeSingleMessage( + ethers.utils.formatBytes32String('messageId1'), + 77, + ethers.utils.defaultAbiCoder.encode(['address'], [accounts[5]]), + bridge.address, + [ + { token: wrappedToken.address, amount: toEther(25) }, + { token: token2.address, amount: toEther(10) }, + ] + ) + await offRamp.executeSingleMessage( + ethers.utils.formatBytes32String('messageId2'), + 77, + ethers.utils.defaultAbiCoder.encode(['address'], [accounts[5]]), + bridge.address, + [{ token: token2.address, amount: toEther(10) }] + ) + await offRamp.executeSingleMessage( + ethers.utils.formatBytes32String('messageId3'), + 77, + '0x', + bridge.address, + [{ token: wrappedToken.address, amount: toEther(25) }] + ) + + let events: any = await bridge.queryFilter(bridge.filters['MessageFailed(bytes32,bytes)']()) + + await bridge.retryFailedMessage(events[1].args.messageId, accounts[4]) + assert.equal(await bridge.messageErrorsStatus(events[1].args.messageId), 0) + assert.equal(fromEther(await token2.balanceOf(accounts[4])), 10) + + await bridge.retryFailedMessage(events[2].args.messageId, accounts[5]) + assert.equal(await bridge.messageErrorsStatus(events[2].args.messageId), 0) + assert.equal(fromEther(await wrappedToken.balanceOf(accounts[5])), 25) + + await bridge.retryFailedMessage(events[0].args.messageId, accounts[6]) + assert.equal(await bridge.messageErrorsStatus(events[1].args.messageId), 0) + assert.equal(fromEther(await token2.balanceOf(accounts[6])), 10) + assert.equal(fromEther(await wrappedToken.balanceOf(accounts[6])), 25) + }) + + it('recoverTokens should work correctly', async () => { + await linkToken.transfer(bridge.address, toEther(1000)) + await stakingPool.transfer(bridge.address, toEther(2000)) + await bridge.recoverTokens([linkToken.address, stakingPool.address], accounts[3]) + + assert.equal(fromEther(await linkToken.balanceOf(accounts[3])), 1000) + assert.equal(fromEther(await stakingPool.balanceOf(accounts[3])), 2000) + }) +}) From e9e09aaf0e2aed6910a74050004d9670a3a7af9e Mon Sep 17 00:00:00 2001 From: BkChoy Date: Sun, 22 Oct 2023 16:30:46 -0400 Subject: [PATCH 06/42] added comments to wrapped token bridge --- contracts/core/ccip/WrappedTokenBridge.sol | 96 +++++++++++++++++++--- 1 file changed, 86 insertions(+), 10 deletions(-) diff --git a/contracts/core/ccip/WrappedTokenBridge.sol b/contracts/core/ccip/WrappedTokenBridge.sol index aa448870..1e3521fa 100644 --- a/contracts/core/ccip/WrappedTokenBridge.sol +++ b/contracts/core/ccip/WrappedTokenBridge.sol @@ -9,6 +9,13 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../interfaces/IWrappedLST.sol"; +/** + * @title Wrapped token bridge + * @notice Handles CCIP transfers with a wrapped token + * @dev This contract can perform 2 functions: + * - can wrap tokens and initiate a CCIP transfer of the wrapped tokens to a destination chain + * - can receive a CCIP transfer of wrapped tokens, unwrap them, and send them to the receiver + */ contract WrappedTokenBridge is Ownable, CCIPReceiver { using SafeERC20 for IERC20; @@ -58,6 +65,13 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { _; } + /** + * @notice Initializes the contract + * @param _router address of the CCIP router + * @param _linkToken address of the LINK token + * @param _token address of the unwrapped token + * @param _wrappedToken address of the wrapped token + **/ constructor( address _router, address _linkToken, @@ -74,6 +88,13 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { wrappedToken.approve(_router, type(uint256).max); } + /** + * @notice ERC677 implementation to receive a token transfer to be wrapped and sent to a destination chain + * @param _sender address of sender + * @param _value amount of tokens transferred + * @param _calldata encoded calldata consisting of destinationChainSelector (uint64), receiver (address), + * maxLINKFee (uint256), extraArgs (bytes) + **/ function onTokenTransfer( address _sender, uint256 _value, @@ -89,6 +110,15 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { _transferTokens(destinationChainSelector, _sender, receiver, _value, false, maxLINKFee, extraArgs); } + /** + * @notice Wraps and transfers tokens to a destination chain + * @param _destinationChainSelector id of destination chain + * @param _receiver address to receive tokens on destination chain + * @param _amount amount of tokens to transfer + * @param _payNative whether fee should be paid natively or with LINK + * @param _maxLINKFee call will revert if LINK fee exceeds this value + * @param _extraArgs encoded args as defined in CCIP API + **/ function transferTokens( uint64 _destinationChainSelector, address _receiver, @@ -102,6 +132,13 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { _transferTokens(_destinationChainSelector, msg.sender, _receiver, _amount, _payNative, _maxLINKFee, _extraArgs); } + /** + * @notice Returns the current fee for a token transfer + * @param _destinationChainSelector id of destination chain + * @param _payNative whether fee should be paid natively or with LINK + * @param _extraArgs encoded args as defined in CCIP API + * @return fee current fee + **/ function getFee( uint64 _destinationChainSelector, bool _payNative, @@ -117,6 +154,10 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { return IRouterClient(this.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); } + /** + * @notice Called by the CCIP router to deliver a message + * @param _any2EvmMessage CCIP message + **/ function ccipReceive(Client.Any2EVMMessage calldata _any2EvmMessage) external override onlyRouter { try this.processMessage(_any2EvmMessage) {} catch (bytes memory err) { bytes32 messageId = _any2EvmMessage.messageId; @@ -126,23 +167,37 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { } } + /** + * @notice Processes a received message + * @param _any2EvmMessage CCIP message + **/ function processMessage(Client.Any2EVMMessage calldata _any2EvmMessage) external onlySelf { _ccipReceive(_any2EvmMessage); } - function retryFailedMessage(bytes32 _messageId, address tokenReceiver) external onlyOwner { + /** + * @notice Executes a failed message + * @param _messageId id of CCIP message + * @param _tokenReceiver address to receive all token transfers included in the message + **/ + function retryFailedMessage(bytes32 _messageId, address _tokenReceiver) external onlyOwner { if (messageErrorsStatus[_messageId] != ErrorStatus.UNRESOLVED) revert MessageIsResolved(); messageErrorsStatus[_messageId] = ErrorStatus.RESOLVED; Client.Any2EVMMessage memory message = failedMessages[_messageId]; for (uint256 i = 0; i < message.destTokenAmounts.length; ++i) { - IERC20(message.destTokenAmounts[i].token).safeTransfer(tokenReceiver, message.destTokenAmounts[i].amount); + IERC20(message.destTokenAmounts[i].token).safeTransfer(_tokenReceiver, message.destTokenAmounts[i].amount); } emit MessageResolved(_messageId); } + /** + * @notice Recovers tokens that were accidentally sent to this contract + * @param _tokens list of tokens to recover + * @param _receiver address to receive recovered tokens + **/ function recoverTokens(address[] calldata _tokens, address _receiver) external onlyOwner { for (uint256 i = 0; i < _tokens.length; ++i) { IERC20 tokenToTransfer = IERC20(_tokens[i]); @@ -150,6 +205,16 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { } } + /** + * @notice Wraps and transfers tokens to a destination chain + * @param _destinationChainSelector id of destination chain + * @param _sender address of token sender + * @param _receiver address to receive tokens on destination chain + * @param _amount amount of tokens to transfer + * @param _payNative whether fee should be paid natively or with LINK + * @param _maxLINKFee call will revert if LINK fee exceeds this value + * @param _extraArgs encoded args as defined in CCIP API + **/ function _transferTokens( uint64 _destinationChainSelector, address _sender, @@ -198,6 +263,13 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { return messageId; } + /** + * @notice Builds a CCIP message + * @param _receiver address to receive tokens on destination chain + * @param _amount amount of tokens to transfer + * @param _feeTokenAddress address of token that fees will be paid in + * @param _extraArgs encoded args as defined in CCIP API + **/ function _buildCCIPMessage( address _receiver, uint256 _amount, @@ -219,12 +291,16 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { return evm2AnyMessage; } - function _ccipReceive(Client.Any2EVMMessage memory any2EvmMessage) internal override { - if (any2EvmMessage.destTokenAmounts.length != 1) revert InvalidMessage(); + /** + * @notice Processes a received message + * @param _any2EvmMessage CCIP message + **/ + function _ccipReceive(Client.Any2EVMMessage memory _any2EvmMessage) internal override { + if (_any2EvmMessage.destTokenAmounts.length != 1) revert InvalidMessage(); - address tokenAddress = any2EvmMessage.destTokenAmounts[0].token; - uint256 tokenAmount = any2EvmMessage.destTokenAmounts[0].amount; - address receiver = abi.decode(any2EvmMessage.data, (address)); + address tokenAddress = _any2EvmMessage.destTokenAmounts[0].token; + uint256 tokenAmount = _any2EvmMessage.destTokenAmounts[0].amount; + address receiver = abi.decode(_any2EvmMessage.data, (address)); if (tokenAddress != address(wrappedToken) || receiver == address(0)) revert InvalidMessage(); @@ -234,9 +310,9 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { token.safeTransfer(receiver, amountToTransfer); emit TokensReceived( - any2EvmMessage.messageId, - any2EvmMessage.sourceChainSelector, - abi.decode(any2EvmMessage.sender, (address)), + _any2EvmMessage.messageId, + _any2EvmMessage.sourceChainSelector, + abi.decode(_any2EvmMessage.sender, (address)), receiver, tokenAmount ); From 827b0cfdc1defcb09fa7139e3920197f2606ee05 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Thu, 26 Oct 2023 20:32:33 -0400 Subject: [PATCH 07/42] reSDL token bridge with tests --- contracts/core/ccip/RESDLTokenBridge.sol | 305 ++++++++++++++++ contracts/core/interfaces/ISDLPool.sol | 26 ++ .../interfaces/ISDLPoolCCIPController.sol | 24 ++ contracts/core/sdlPool/SDLPool.sol | 59 ++++ .../core/test/SDLPoolCCIPControllerMock.sol | 58 ++++ test/core/ccip/resdl-token-bridge.test.ts | 328 ++++++++++++++++++ 6 files changed, 800 insertions(+) create mode 100644 contracts/core/ccip/RESDLTokenBridge.sol create mode 100644 contracts/core/interfaces/ISDLPoolCCIPController.sol create mode 100644 contracts/core/test/SDLPoolCCIPControllerMock.sol create mode 100644 test/core/ccip/resdl-token-bridge.test.ts diff --git a/contracts/core/ccip/RESDLTokenBridge.sol b/contracts/core/ccip/RESDLTokenBridge.sol new file mode 100644 index 00000000..004e1108 --- /dev/null +++ b/contracts/core/ccip/RESDLTokenBridge.sol @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol"; +import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; +import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../interfaces/ISDLPool.sol"; +import "../interfaces/ISDLPoolCCIPController.sol"; + +/** + * @title reSDL Token Bridge + * @notice Handles CCIP transfers of reSDL NFTs + */ +contract RESDLTokenBridge is Ownable, CCIPReceiver { + using SafeERC20 for IERC20; + + struct RESDLToken { + uint256 amount; + uint256 boostAmount; + uint64 startTime; + uint64 duration; + uint64 expiry; + } + + IERC20 public linkToken; + + IERC20 public sdlToken; + ISDLPool public sdlPool; + ISDLPoolCCIPController public sdlPoolCCIPController; + + mapping(uint64 => address) public whitelistedDestinations; + + event TokenTransferred( + bytes32 indexed messageId, + uint64 indexed destinationChainSelector, + address indexed sender, + address receiver, + uint256 tokenId, + address feeToken, + uint256 fees + ); + event TokenReceived( + bytes32 indexed messageId, + uint64 indexed destinationChainSelector, + address indexed sender, + address receiver, + uint256 tokenId + ); + event MessageFailed(bytes32 indexed messageId, bytes error); + event DestinationAdded(uint64 indexed destinationChainSelector, address destination); + event DestinationRemoved(uint64 indexed destinationChainSelector, address destination); + + error InsufficientFee(); + error TransferFailed(); + error FeeExceedsLimit(); + error OnlySelf(); + error SenderNotAuthorized(); + error InvalidDestination(); + error InvalidReceiver(); + error AlreadyAdded(); + error AlreadyRemoved(); + + modifier onlySelf() { + if (msg.sender != address(this)) revert OnlySelf(); + _; + } + + /** + * @notice Initializes the contract + * @param _router address of the CCIP router + * @param _linkToken address of the LINK token + * @param _sdlToken address of the SDL token + * @param _sdlPool address of the SDL Pool + * @param _sdlPoolCCIPController address of the SDL Pool CCIP controller + **/ + constructor( + address _router, + address _linkToken, + address _sdlToken, + address _sdlPool, + address _sdlPoolCCIPController + ) CCIPReceiver(_router) { + linkToken = IERC20(_linkToken); + sdlToken = IERC20(_sdlToken); + sdlPool = ISDLPool(_sdlPool); + sdlPoolCCIPController = ISDLPoolCCIPController(_sdlPoolCCIPController); + linkToken.safeApprove(_router, type(uint256).max); + sdlToken.safeApprove(_router, type(uint256).max); + sdlToken.safeApprove(_sdlPoolCCIPController, type(uint256).max); + } + + /** + * @notice Transfers an reSDL token to a destination chain + * @param _destinationChainSelector id of destination chain + * @param _receiver address to receive reSDL on destination chain + * @param _tokenId id of reSDL token + * @param _payNative whether fee should be paid natively or with LINK + * @param _maxLINKFee call will revert if LINK fee exceeds this value + * @param _extraArgs encoded args as defined in CCIP API + **/ + function transferRESDL( + uint64 _destinationChainSelector, + address _receiver, + uint256 _tokenId, + bool _payNative, + uint256 _maxLINKFee, + bytes memory _extraArgs + ) external payable returns (bytes32 messageId) { + address sender = msg.sender; + if (sender != sdlPool.ownerOf(_tokenId)) revert SenderNotAuthorized(); + if (_receiver == address(0)) revert InvalidReceiver(); + + address destination = whitelistedDestinations[_destinationChainSelector]; + if (destination == address(0)) revert InvalidDestination(); + + RESDLToken memory reSDLToken; + { + (uint256 amount, uint256 boostAmount, uint64 startTime, uint64 duration, uint64 expiry) = sdlPoolCCIPController + .handleOutgoingRESDL(sender, _tokenId); + reSDLToken = RESDLToken(amount, boostAmount, startTime, duration, expiry); + } + + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + _receiver, + _tokenId, + reSDLToken, + destination, + _payNative ? address(0) : address(linkToken), + _extraArgs + ); + + IRouterClient router = IRouterClient(this.getRouter()); + uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage); + + if (_payNative) { + if (fees > msg.value) revert InsufficientFee(); + messageId = router.ccipSend{value: fees}(_destinationChainSelector, evm2AnyMessage); + if (fees < msg.value) { + (bool success, ) = sender.call{value: msg.value - fees}(""); + if (!success) revert TransferFailed(); + } + } else { + if (fees > _maxLINKFee) revert FeeExceedsLimit(); + linkToken.safeTransferFrom(sender, address(this), fees); + messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage); + } + + emit TokenTransferred( + messageId, + _destinationChainSelector, + sender, + _receiver, + _tokenId, + _payNative ? address(0) : address(linkToken), + fees + ); + } + + /** + * @notice Returns the current fee for an reSDL transfer + * @param _destinationChainSelector id of destination chain + * @param _payNative whether fee should be paid natively or with LINK + * @param _extraArgs encoded args as defined in CCIP API + * @return fee current fee + **/ + function getFee( + uint64 _destinationChainSelector, + bool _payNative, + bytes memory _extraArgs + ) external view returns (uint256) { + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + address(this), + 0, + RESDLToken(0, 0, 0, 0, 0), + address(this), + _payNative ? address(0) : address(linkToken), + _extraArgs + ); + + return IRouterClient(this.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); + } + + /** + * @notice Recovers tokens that were accidentally sent to this contract + * @param _tokens list of tokens to recover + * @param _receiver address to receive recovered tokens + **/ + function recoverTokens(address[] calldata _tokens, address _receiver) external onlyOwner { + for (uint256 i = 0; i < _tokens.length; ++i) { + IERC20 tokenToTransfer = IERC20(_tokens[i]); + tokenToTransfer.safeTransfer(_receiver, tokenToTransfer.balanceOf(address(this))); + } + } + + /** + * @notice Whitelists a new destination chain + * @param _destinationChainSelector id of destination chain + * @param _destination address to receive CCIP messages on destination chain + **/ + function addWhitelistedDestination(uint64 _destinationChainSelector, address _destination) external onlyOwner { + if (whitelistedDestinations[_destinationChainSelector] != address(0)) revert AlreadyAdded(); + if (_destination == address(0)) revert InvalidDestination(); + whitelistedDestinations[_destinationChainSelector] = _destination; + emit DestinationAdded(_destinationChainSelector, _destination); + } + + /** + * @notice Removes an existing destination chain + * @param _destinationChainSelector id of destination chain + **/ + function removeWhitelistedDestination(uint64 _destinationChainSelector) external onlyOwner { + if (whitelistedDestinations[_destinationChainSelector] == address(0)) revert AlreadyRemoved(); + emit DestinationRemoved(_destinationChainSelector, whitelistedDestinations[_destinationChainSelector]); + delete whitelistedDestinations[_destinationChainSelector]; + } + + /** + * @notice Called by the CCIP router to deliver a message + * @param _any2EvmMessage CCIP message + **/ + function ccipReceive(Client.Any2EVMMessage calldata _any2EvmMessage) external override onlyRouter { + try this.processMessage(_any2EvmMessage) {} catch (bytes memory err) { + emit MessageFailed(_any2EvmMessage.messageId, err); + } + } + + /** + * @notice Processes a received message + * @param _any2EvmMessage CCIP message + **/ + function processMessage(Client.Any2EVMMessage calldata _any2EvmMessage) external onlySelf { + _ccipReceive(_any2EvmMessage); + } + + /** + * @notice Builds a CCIP message + * @dev builds the message for outgoing reSDL transfers + * @param _receiver address to receive reSDL token on destination chain + * @param _tokenId id of reSDL token + * @param _reSDLToken reSDL token + * @param _destination address of destination contract + * @param _feeTokenAddress address of token that fees will be paid in + * @param _extraArgs encoded args as defined in CCIP API + **/ + function _buildCCIPMessage( + address _receiver, + uint256 _tokenId, + RESDLToken memory _reSDLToken, + address _destination, + address _feeTokenAddress, + bytes memory _extraArgs + ) internal view returns (Client.EVM2AnyMessage memory) { + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); + Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({ + token: address(sdlToken), + amount: _reSDLToken.amount + }); + tokenAmounts[0] = tokenAmount; + + Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(_destination), + data: abi.encode( + _receiver, + _tokenId, + _reSDLToken.amount, + _reSDLToken.boostAmount, + _reSDLToken.startTime, + _reSDLToken.duration, + _reSDLToken.expiry + ), + tokenAmounts: tokenAmounts, + extraArgs: _extraArgs, + feeToken: _feeTokenAddress + }); + + return evm2AnyMessage; + } + + /** + * @notice Processes a received message + * @dev handles incoming reSDL transfers + * @param _any2EvmMessage CCIP message + **/ + function _ccipReceive(Client.Any2EVMMessage memory _any2EvmMessage) internal override { + address sender = abi.decode(_any2EvmMessage.sender, (address)); + if (sender != whitelistedDestinations[_any2EvmMessage.sourceChainSelector]) revert SenderNotAuthorized(); + + ( + address receiver, + uint256 tokenId, + uint256 amount, + uint256 boostAmount, + uint64 startTime, + uint64 duration, + uint64 expiry + ) = abi.decode(_any2EvmMessage.data, (address, uint256, uint256, uint256, uint64, uint64, uint64)); + + sdlPoolCCIPController.handleIncomingRESDL(receiver, tokenId, amount, boostAmount, startTime, duration, expiry); + + emit TokenReceived(_any2EvmMessage.messageId, _any2EvmMessage.sourceChainSelector, sender, receiver, tokenId); + } +} diff --git a/contracts/core/interfaces/ISDLPool.sol b/contracts/core/interfaces/ISDLPool.sol index f41df6fb..6f0259f6 100644 --- a/contracts/core/interfaces/ISDLPool.sol +++ b/contracts/core/interfaces/ISDLPool.sol @@ -3,4 +3,30 @@ pragma solidity 0.8.15; interface ISDLPool { function effectiveBalanceOf(address _account) external view returns (uint256); + + function ownerOf(uint256 _lockId) external view returns (address); + + function burn( + address _sender, + uint256 _lockId, + address _sdlReceiver + ) + external + returns ( + uint256 _amount, + uint256 _boostAmount, + uint64 _startTime, + uint64 _duration, + uint64 _expiry + ); + + function mint( + address _receiver, + uint256 _lockId, + uint256 _amount, + uint256 _boostAmount, + uint64 _startTime, + uint64 _duration, + uint64 _expiry + ) external; } diff --git a/contracts/core/interfaces/ISDLPoolCCIPController.sol b/contracts/core/interfaces/ISDLPoolCCIPController.sol new file mode 100644 index 00000000..467a2211 --- /dev/null +++ b/contracts/core/interfaces/ISDLPoolCCIPController.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.15; + +interface ISDLPoolCCIPController { + function handleOutgoingRESDL(address _sender, uint256 _lockId) + external + returns ( + uint256 _amount, + uint256 _boostAmount, + uint64 _startTime, + uint64 _duration, + uint64 _expiry + ); + + function handleIncomingRESDL( + address _receiver, + uint256 _lockId, + uint256 _amount, + uint256 _boostAmount, + uint64 _startTime, + uint64 _duration, + uint64 _expiry + ) external; +} diff --git a/contracts/core/sdlPool/SDLPool.sol b/contracts/core/sdlPool/SDLPool.sol index 9356c530..f2f73f09 100644 --- a/contracts/core/sdlPool/SDLPool.sol +++ b/contracts/core/sdlPool/SDLPool.sol @@ -44,6 +44,8 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp string public baseURI; + address public ccipController; + event InitiateUnlock(address indexed owner, uint256 indexed lockId, uint64 expiry); event Withdraw(address indexed owner, uint256 indexed lockId, uint256 amount); event CreateLock( @@ -79,6 +81,12 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp error DuplicateContract(); error ContractNotFound(); error UnlockAlreadyInitiated(); + error OnlyCCIPController(); + + modifier onlyCCIPController() { + if (msg.sender != ccipController) revert OnlyCCIPController(); + _; + } /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -312,6 +320,53 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp sdlToken.safeTransfer(msg.sender, _amount); } + function mint( + address _receiver, + uint256 _lockId, + uint256 _amount, + uint256 _boostAmount, + uint64 _startTime, + uint64 _duration, + uint64 _expiry + ) external onlyCCIPController updateRewards(_receiver) updateRewards(ccipController) { + if (lockOwners[_lockId] != address(0)) revert InvalidLockId(); + + locks[_lockId] = Lock(_amount, _boostAmount, _startTime, _duration, _expiry); + lockOwners[_lockId] = _receiver; + balances[_receiver] += 1; + + uint256 totalAmount = _amount + _boostAmount; + effectiveBalances[_receiver] += totalAmount; + effectiveBalances[ccipController] -= totalAmount; + } + + function burn( + address _sender, + uint256 _lockId, + address _sdlReceiver + ) + external + onlyCCIPController + onlyLockOwner(_lockId, _sender) + updateRewards(_sender) + updateRewards(ccipController) + returns (Lock memory) + { + Lock memory lock = locks[_lockId]; + + delete locks[_lockId].amount; + delete lockOwners[_lockId]; + balances[_sender] -= 1; + + uint256 totalAmount = lock.amount + lock.boostAmount; + effectiveBalances[_sender] -= totalAmount; + effectiveBalances[ccipController] += totalAmount; + + sdlToken.safeTransfer(_sdlReceiver, lock.amount); + + return lock; + } + /** * @notice transfers a lock between accounts * @dev reverts if sender is not the owner of and not approved to transfer the lock @@ -482,6 +537,10 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp boostController = IBoostController(_boostController); } + function setCCIPController(address _ccipController) external onlyOwner { + ccipController = _ccipController; + } + /** * @notice used by the delegator pool to migrate user stakes to this contract * @dev diff --git a/contracts/core/test/SDLPoolCCIPControllerMock.sol b/contracts/core/test/SDLPoolCCIPControllerMock.sol new file mode 100644 index 00000000..f4563793 --- /dev/null +++ b/contracts/core/test/SDLPoolCCIPControllerMock.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../interfaces/ISDLPool.sol"; + +contract SDLPoolCCIPControllerMock { + using SafeERC20 for IERC20; + + IERC20 public sdlToken; + ISDLPool public sdlPool; + address public reSDLTokenBridge; + + error OnlySelf(); + error OnlyRESDLTokenBridge(); + + modifier onlyBridge() { + if (msg.sender != reSDLTokenBridge) revert OnlyRESDLTokenBridge(); + _; + } + + constructor(address _sdlToken, address _sdlPool) { + sdlToken = IERC20(_sdlToken); + sdlPool = ISDLPool(_sdlPool); + } + + function handleOutgoingRESDL(address _sender, uint256 _tokenId) + external + onlyBridge + returns ( + uint256, + uint256, + uint64, + uint64, + uint64 + ) + { + return sdlPool.burn(_sender, _tokenId, reSDLTokenBridge); + } + + function handleIncomingRESDL( + address _receiver, + uint256 _tokenId, + uint256 _amount, + uint256 _boostAmount, + uint64 _startTime, + uint64 _duration, + uint64 _expiry + ) external onlyBridge { + sdlToken.safeTransferFrom(reSDLTokenBridge, address(sdlPool), _amount); + sdlPool.mint(_receiver, _tokenId, _amount, _boostAmount, _startTime, _duration, _expiry); + } + + function setRESDLTokenBridge(address _reSDLTokenBridge) external { + reSDLTokenBridge = _reSDLTokenBridge; + } +} diff --git a/test/core/ccip/resdl-token-bridge.test.ts b/test/core/ccip/resdl-token-bridge.test.ts new file mode 100644 index 00000000..c5333667 --- /dev/null +++ b/test/core/ccip/resdl-token-bridge.test.ts @@ -0,0 +1,328 @@ +import { ethers } from 'hardhat' +import { assert, expect } from 'chai' +import { toEther, deploy, deployUpgradeable, getAccounts, fromEther } from '../../utils/helpers' +import { + ERC677, + CCIPOnRampMock, + CCIPOffRampMock, + CCIPTokenPoolMock, + WrappedNative, + RESDLTokenBridge, + SDLPool, + SDLPoolCCIPControllerMock, +} from '../../../typechain-types' +import { time } from '@nomicfoundation/hardhat-network-helpers' +import { Signer } from 'ethers' + +describe('RESDLTokenBridge', () => { + let linkToken: ERC677 + let sdlToken: ERC677 + let token2: ERC677 + let bridge: RESDLTokenBridge + let sdlPool: SDLPool + let onRamp: CCIPOnRampMock + let offRamp: CCIPOffRampMock + let tokenPool: CCIPTokenPoolMock + let tokenPool2: CCIPTokenPoolMock + let wrappedNative: WrappedNative + let accounts: string[] + let signers: Signer[] + + before(async () => { + ;({ signers, accounts } = await getAccounts()) + }) + + beforeEach(async () => { + linkToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + sdlToken = (await deploy('ERC677', ['SDL', 'SDL', 1000000000])) as ERC677 + token2 = (await deploy('ERC677', ['2', '2', 1000000000])) as ERC677 + + wrappedNative = (await deploy('WrappedNative')) as WrappedNative + const armProxy = await deploy('CCIPArmProxyMock') + const router = await deploy('Router', [wrappedNative.address, armProxy.address]) + tokenPool = (await deploy('CCIPTokenPoolMock', [sdlToken.address])) as CCIPTokenPoolMock + tokenPool2 = (await deploy('CCIPTokenPoolMock', [token2.address])) as CCIPTokenPoolMock + onRamp = (await deploy('CCIPOnRampMock', [ + [sdlToken.address, token2.address], + [tokenPool.address, tokenPool2.address], + linkToken.address, + ])) as CCIPOnRampMock + offRamp = (await deploy('CCIPOffRampMock', [ + router.address, + [sdlToken.address, token2.address], + [tokenPool.address, tokenPool2.address], + ])) as CCIPOffRampMock + + await router.applyRampUpdates([[77, onRamp.address]], [], [[77, offRamp.address]]) + + let boostController = await deploy('LinearBoostController', [4 * 365 * 86400, 4]) + sdlPool = (await deployUpgradeable('SDLPool', [ + 'reSDL', + 'reSDL', + sdlToken.address, + boostController.address, + ethers.constants.AddressZero, + ])) as SDLPool + let sdlPoolCCIPController = (await deploy('SDLPoolCCIPControllerMock', [ + sdlToken.address, + sdlPool.address, + ])) as SDLPoolCCIPControllerMock + + bridge = (await deploy('RESDLTokenBridge', [ + router.address, + linkToken.address, + sdlToken.address, + sdlPool.address, + sdlPoolCCIPController.address, + ])) as RESDLTokenBridge + + await sdlPoolCCIPController.setRESDLTokenBridge(bridge.address) + await sdlPool.setCCIPController(sdlPoolCCIPController.address) + await linkToken.approve(bridge.address, ethers.constants.MaxUint256) + await bridge.addWhitelistedDestination(77, accounts[0]) + await sdlToken.transfer(accounts[1], toEther(200)) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(1000), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) + ) + }) + + it('getFee should work correctly', async () => { + assert.equal(fromEther(await bridge.getFee(77, false, '0x')), 2) + assert.equal(fromEther(await bridge.getFee(77, true, '0x')), 3) + await expect(bridge.getFee(78, false, '0x')).to.be.reverted + await expect(bridge.getFee(78, true, '0x')).to.be.reverted + }) + + it('transferRESDL should work correctly with LINK fee', async () => { + let ts1 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await time.setNextBlockTimestamp(ts1 + 365 * 86400) + await sdlPool.initiateUnlock(2) + let ts2 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + + let preFeeBalance = await linkToken.balanceOf(accounts[0]) + + await bridge.transferRESDL(77, accounts[4], 2, false, toEther(10), '0x') + let lastRequestData = await onRamp.lastRequestData() + let lastRequestMsg = await onRamp.getLastRequestMessage() + + assert.equal(fromEther(await sdlToken.balanceOf(tokenPool.address)), 1000) + assert.equal(fromEther(preFeeBalance.sub(await linkToken.balanceOf(accounts[0]))), 2) + + assert.equal(fromEther(lastRequestData[0]), 2) + assert.equal(lastRequestData[1], bridge.address) + + assert.equal( + ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], + accounts[0] + ) + assert.deepEqual( + ethers.utils.defaultAbiCoder + .decode( + ['address', 'uint256', 'uint256', 'uint256', 'uint64', 'uint64', 'uint64'], + lastRequestMsg[1] + ) + .map((d, i) => { + if (i == 0) return d + if (i > 1 && i < 4) return fromEther(d) + return d.toNumber() + }), + [accounts[4], 2, 1000, 0, ts1, 365 * 86400, ts2 + (365 * 86400) / 2] + ) + assert.deepEqual( + lastRequestMsg[2].map((d) => [d.token, fromEther(d.amount)]), + [[sdlToken.address, 1000]] + ) + assert.equal(lastRequestMsg[3], linkToken.address) + await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') + + await expect( + bridge.transferRESDL(77, accounts[4], 1, false, toEther(1), '0x') + ).to.be.revertedWith('FeeExceedsLimit()') + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(500), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 2 * 365 * 86400]) + ) + let ts3 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + + preFeeBalance = await linkToken.balanceOf(accounts[0]) + + await bridge.transferRESDL(77, accounts[5], 3, false, toEther(10), '0x') + lastRequestData = await onRamp.lastRequestData() + lastRequestMsg = await onRamp.getLastRequestMessage() + + assert.equal(fromEther(await sdlToken.balanceOf(tokenPool.address)), 1500) + assert.equal(fromEther(preFeeBalance.sub(await linkToken.balanceOf(accounts[0]))), 2) + + assert.equal(fromEther(lastRequestData[0]), 2) + assert.equal(lastRequestData[1], bridge.address) + + assert.equal( + ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], + accounts[0] + ) + assert.deepEqual( + ethers.utils.defaultAbiCoder + .decode( + ['address', 'uint256', 'uint256', 'uint256', 'uint64', 'uint64', 'uint64'], + lastRequestMsg[1] + ) + .map((d, i) => { + if (i == 0) return d + if (i > 1 && i < 4) return fromEther(d) + return d.toNumber() + }), + [accounts[5], 3, 500, 1000, ts3, 2 * 365 * 86400, 0] + ) + assert.deepEqual( + lastRequestMsg[2].map((d) => [d.token, fromEther(d.amount)]), + [[sdlToken.address, 500]] + ) + assert.equal(lastRequestMsg[3], linkToken.address) + await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') + }) + + it('transferTokens should work correctly with native fee', async () => { + let ts = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + + let preFeeBalance = await ethers.provider.getBalance(accounts[0]) + + await bridge.transferRESDL(77, accounts[4], 2, true, toEther(10), '0x', { value: toEther(10) }) + let lastRequestData = await onRamp.lastRequestData() + let lastRequestMsg = await onRamp.getLastRequestMessage() + + assert.equal(fromEther(await sdlToken.balanceOf(tokenPool.address)), 1000) + assert.equal( + Math.trunc(fromEther(preFeeBalance.sub(await ethers.provider.getBalance(accounts[0])))), + 3 + ) + assert.equal(fromEther(lastRequestData[0]), 3) + assert.equal(lastRequestData[1], bridge.address) + + assert.equal( + ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], + accounts[0] + ) + assert.deepEqual( + ethers.utils.defaultAbiCoder + .decode( + ['address', 'uint256', 'uint256', 'uint256', 'uint64', 'uint64', 'uint64'], + lastRequestMsg[1] + ) + .map((d, i) => { + if (i == 0) return d + if (i > 1 && i < 4) return fromEther(d) + return d.toNumber() + }), + [accounts[4], 2, 1000, 1000, ts, 365 * 86400, 0] + ) + assert.deepEqual( + lastRequestMsg[2].map((d) => [d.token, fromEther(d.amount)]), + [[sdlToken.address, 1000]] + ) + assert.equal(lastRequestMsg[3], wrappedNative.address) + await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') + }) + + it('transferRESDL validation should work correctly', async () => { + await expect( + bridge.connect(signers[1]).transferRESDL(77, accounts[4], 1, false, toEther(10), '0x') + ).to.be.revertedWith('SenderNotAuthorized()') + await expect( + bridge.transferRESDL(77, ethers.constants.AddressZero, 1, false, toEther(10), '0x') + ).to.be.revertedWith('InvalidReceiver()') + await expect( + bridge.transferRESDL(78, accounts[4], 1, false, toEther(10), '0x') + ).to.be.revertedWith('InvalidDestination()') + + bridge.transferRESDL(77, accounts[4], 1, false, toEther(10), '0x') + }) + + it('ccipReceive should work correctly', async () => { + await bridge.transferRESDL(77, accounts[4], 2, true, toEther(10), '0x', { value: toEther(10) }) + + await offRamp + .connect(signers[1]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint256', 'uint256', 'uint64', 'uint64', 'uint64'], + [accounts[5], 2, toEther(25), toEther(25), 1000, 3000, 8000] + ), + bridge.address, + [{ token: sdlToken.address, amount: toEther(25) }] + ) + + let events: any = await bridge.queryFilter(bridge.filters['MessageFailed(bytes32,bytes)']()) + assert.equal(events[0].args.messageId, ethers.utils.formatBytes32String('messageId')) + assert.equal(fromEther(await sdlToken.balanceOf(bridge.address)), 25) + + await offRamp.executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint256', 'uint256', 'uint64', 'uint64', 'uint64'], + [accounts[5], 2, toEther(25), toEther(25), 1000, 3000, 8000] + ), + bridge.address, + [{ token: sdlToken.address, amount: toEther(25) }] + ) + + assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 225) + assert.equal(await sdlPool.ownerOf(2), accounts[5]) + assert.deepEqual( + (await sdlPool.getLocks([2])).map((l: any) => ({ + amount: fromEther(l.amount), + boostAmount: Number(fromEther(l.boostAmount).toFixed(4)), + startTime: l.startTime.toNumber(), + duration: l.duration.toNumber(), + expiry: l.expiry.toNumber(), + })), + [ + { + amount: 25, + boostAmount: 25, + startTime: 1000, + duration: 3000, + expiry: 8000, + }, + ] + ) + }) + + it('should be able to add/remove whitelisted destinations', async () => { + await expect( + bridge.addWhitelistedDestination(10, ethers.constants.AddressZero) + ).to.be.revertedWith('InvalidDestination()') + await expect(bridge.removeWhitelistedDestination(10)).to.be.revertedWith('AlreadyRemoved()') + + await bridge.addWhitelistedDestination(10, accounts[0]) + + assert.equal(await bridge.whitelistedDestinations(10), accounts[0]) + await expect(bridge.addWhitelistedDestination(10, accounts[1])).to.be.revertedWith( + 'AlreadyAdded()' + ) + + await bridge.removeWhitelistedDestination(10) + assert.equal(await bridge.whitelistedDestinations(10), ethers.constants.AddressZero) + }) + + it('recoverTokens should work correctly', async () => { + await linkToken.transfer(bridge.address, toEther(1000)) + await sdlToken.transfer(bridge.address, toEther(2000)) + await bridge.recoverTokens([linkToken.address, sdlToken.address], accounts[3]) + + assert.equal(fromEther(await linkToken.balanceOf(accounts[3])), 1000) + assert.equal(fromEther(await sdlToken.balanceOf(accounts[3])), 2000) + }) +}) From ccb729da899af9c94163f098224adcf25d899b85 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Thu, 2 Nov 2023 14:33:13 -0400 Subject: [PATCH 08/42] updated token bridge events --- contracts/core/ccip/RESDLTokenBridge.sol | 2 +- contracts/core/ccip/WrappedTokenBridge.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/core/ccip/RESDLTokenBridge.sol b/contracts/core/ccip/RESDLTokenBridge.sol index 004e1108..2d61e21f 100644 --- a/contracts/core/ccip/RESDLTokenBridge.sol +++ b/contracts/core/ccip/RESDLTokenBridge.sol @@ -44,7 +44,7 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { ); event TokenReceived( bytes32 indexed messageId, - uint64 indexed destinationChainSelector, + uint64 indexed sourceChainSelector, address indexed sender, address receiver, uint256 tokenId diff --git a/contracts/core/ccip/WrappedTokenBridge.sol b/contracts/core/ccip/WrappedTokenBridge.sol index 1e3521fa..5b32551a 100644 --- a/contracts/core/ccip/WrappedTokenBridge.sol +++ b/contracts/core/ccip/WrappedTokenBridge.sol @@ -43,7 +43,7 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { ); event TokensReceived( bytes32 indexed messageId, - uint64 indexed destinationChainSelector, + uint64 indexed sourceChainSelector, address indexed sender, address receiver, uint256 tokenAmount From e2f01ad6abbbe1bb5c40a4dcea0c8b4b232c2131 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Fri, 3 Nov 2023 14:03:34 -0400 Subject: [PATCH 09/42] sdl pool ccip integration --- contracts/core/sdlPool/SDLPool.sol | 574 +++----------------- contracts/core/sdlPool/SDLPoolSecondary.sol | 424 +++++++++++++++ contracts/core/sdlPool/base/SDLPoolBase.sol | 502 +++++++++++++++++ 3 files changed, 1004 insertions(+), 496 deletions(-) create mode 100644 contracts/core/sdlPool/SDLPoolSecondary.sol create mode 100644 contracts/core/sdlPool/base/SDLPoolBase.sol diff --git a/contracts/core/sdlPool/SDLPool.sol b/contracts/core/sdlPool/SDLPool.sol index f2f73f09..54ee74ed 100644 --- a/contracts/core/sdlPool/SDLPool.sol +++ b/contracts/core/sdlPool/SDLPool.sol @@ -1,92 +1,19 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.15; -import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/IERC721MetadataUpgradeable.sol"; - -import "../base/RewardsPoolController.sol"; -import "../interfaces/IBoostController.sol"; -import "../interfaces/IERC721Receiver.sol"; +import "./base/SDLPoolBase.sol"; /** * @title SDL Pool * @notice Allows users to stake/lock SDL tokens and receive a percentage of the protocol's earned rewards + * @dev deployed only on the primary chain */ -contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUpgradeable { +contract SDLPool is SDLPoolBase { using SafeERC20Upgradeable for IERC20Upgradeable; - struct Lock { - uint256 amount; - uint256 boostAmount; - uint64 startTime; - uint64 duration; - uint64 expiry; - } - - string public name; - string public symbol; - - mapping(address => mapping(address => bool)) private operatorApprovals; - mapping(uint256 => address) private tokenApprovals; - - IERC20Upgradeable public sdlToken; - IBoostController public boostController; - - uint256 public lastLockId; - mapping(uint256 => Lock) private locks; - mapping(uint256 => address) private lockOwners; - mapping(address => uint256) private balances; - - uint256 public totalEffectiveBalance; - mapping(address => uint256) private effectiveBalances; - address public delegatorPool; - string public baseURI; - - address public ccipController; - - event InitiateUnlock(address indexed owner, uint256 indexed lockId, uint64 expiry); - event Withdraw(address indexed owner, uint256 indexed lockId, uint256 amount); - event CreateLock( - address indexed owner, - uint256 indexed lockId, - uint256 amount, - uint256 boostAmount, - uint64 lockingDuration - ); - event UpdateLock( - address indexed owner, - uint256 indexed lockId, - uint256 amount, - uint256 boostAmount, - uint64 lockingDuration - ); - - error SenderNotAuthorized(); - error InvalidLockId(); - error InvalidValue(); - error InvalidLockingDuration(); - error InvalidParams(); - error TransferFromIncorrectOwner(); - error TransferToZeroAddress(); - error TransferToNonERC721Implementer(); - error ApprovalToCurrentOwner(); - error ApprovalToCaller(); - error UnauthorizedToken(); - error TotalDurationNotElapsed(); - error HalfDurationNotElapsed(); - error InsufficientBalance(); - error UnlockNotInitiated(); - error DuplicateContract(); - error ContractNotFound(); - error UnlockAlreadyInitiated(); - error OnlyCCIPController(); - - modifier onlyCCIPController() { - if (msg.sender != ccipController) revert OnlyCCIPController(); - _; - } + event IncomingUpdate(uint256 numNewRESDLTokens, int256 totalRESDLSupplyChange, uint256 mintStartIndex); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -97,104 +24,20 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp * @notice initializes contract * @param _name name of the staking derivative token * @param _symbol symbol of the staking derivative token + * @param _sdlToken address of the SDL token * @param _boostController address of the boost controller - * @param _delegatorPool address of the old contract this one will replace **/ function initialize( string memory _name, string memory _symbol, address _sdlToken, - address _boostController, - address _delegatorPool - ) public initializer { - __RewardsPoolController_init(); - name = _name; - symbol = _symbol; - sdlToken = IERC20Upgradeable(_sdlToken); - boostController = IBoostController(_boostController); - delegatorPool = _delegatorPool; - } - - /** - * @notice reverts if `_owner` is not the owner of `_lockId` - **/ - modifier onlyLockOwner(uint256 _lockId, address _owner) { - if (_owner != ownerOf(_lockId)) revert SenderNotAuthorized(); - _; - } - - /** - * @notice returns the effective stake balance of an account - * @dev the effective stake balance includes the actual amount of tokens an - * account has staked across all locks plus any applicable boost gained by locking - * @param _account address of account - * @return effective stake balance - **/ - function effectiveBalanceOf(address _account) external view returns (uint256) { - return effectiveBalances[_account]; - } - - /** - * @notice returns the number of locks owned by an account - * @param _account address of account - * @return total number of locks owned by account - **/ - function balanceOf(address _account) public view returns (uint256) { - return balances[_account]; - } - - /** - * @notice returns the owner of a lock - * @dev reverts if `_lockId` is invalid - * @param _lockId id of the lock - * @return lock owner - **/ - function ownerOf(uint256 _lockId) public view returns (address) { - address owner = lockOwners[_lockId]; - if (owner == address(0)) revert InvalidLockId(); - return owner; - } - - /** - * @notice returns the list of locks that corresponds to `_lockIds` - * @dev reverts if any lockId is invalid - * @param _lockIds list of lock ids - * @return list of locks - **/ - function getLocks(uint256[] calldata _lockIds) external view returns (Lock[] memory) { - Lock[] memory retLocks = new Lock[](_lockIds.length); - - for (uint256 i = 0; i < _lockIds.length; ++i) { - uint256 lockId = _lockIds[i]; - if (lockOwners[lockId] == address(0)) revert InvalidLockId(); - retLocks[i] = locks[lockId]; - } - - return retLocks; - } - - /** - * @notice returns a list of lockIds owned by an account - * @param _owner address of account - * @return list of lockIds - **/ - function getLockIdsByOwner(address _owner) external view returns (uint256[] memory) { - uint256 maxLockId = lastLockId; - uint256 lockCount = balanceOf(_owner); - uint256 lockIdsFound; - uint256[] memory lockIds = new uint256[](lockCount); - - for (uint256 i = 1; i <= maxLockId; ++i) { - if (lockOwners[i] == _owner) { - lockIds[lockIdsFound] = i; - lockIdsFound++; - if (lockIdsFound == lockCount) break; - } + address _boostController + ) public reinitializer(2) { + if (delegatorPool == address(0)) { + __SDLPoolBase_init(_name, _symbol, _sdlToken, _boostController); + } else { + delegatorPool = ccipController; } - - assert(lockIdsFound == lockCount); - - return lockIds; } /** @@ -226,10 +69,10 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp if (msg.sender == address(sdlToken)) { (uint256 lockId, uint64 lockingDuration) = abi.decode(_calldata, (uint256, uint64)); - if (lockId > 0) { - _updateLock(_sender, lockId, _value, lockingDuration); + if (lockId != 0) { + _storeUpdatedLock(_sender, lockId, _value, lockingDuration); } else { - _createLock(_sender, _value, lockingDuration); + _storeNewLock(_sender, _value, lockingDuration); } } else { distributeToken(msg.sender); @@ -247,7 +90,7 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp **/ function extendLockDuration(uint256 _lockId, uint64 _lockingDuration) external { if (_lockingDuration == 0) revert InvalidLockingDuration(); - _updateLock(msg.sender, _lockId, 0, _lockingDuration); + _storeUpdatedLock(msg.sender, _lockId, 0, _lockingDuration); } /** @@ -320,27 +163,7 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp sdlToken.safeTransfer(msg.sender, _amount); } - function mint( - address _receiver, - uint256 _lockId, - uint256 _amount, - uint256 _boostAmount, - uint64 _startTime, - uint64 _duration, - uint64 _expiry - ) external onlyCCIPController updateRewards(_receiver) updateRewards(ccipController) { - if (lockOwners[_lockId] != address(0)) revert InvalidLockId(); - - locks[_lockId] = Lock(_amount, _boostAmount, _startTime, _duration, _expiry); - lockOwners[_lockId] = _receiver; - balances[_receiver] += 1; - - uint256 totalAmount = _amount + _boostAmount; - effectiveBalances[_receiver] += totalAmount; - effectiveBalances[ccipController] -= totalAmount; - } - - function burn( + function handleOutgoingRESDL( address _sender, uint256 _lockId, address _sdlReceiver @@ -364,181 +187,53 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp sdlToken.safeTransfer(_sdlReceiver, lock.amount); - return lock; - } + emit OutgoingRESDL(_sender, _lockId); - /** - * @notice transfers a lock between accounts - * @dev reverts if sender is not the owner of and not approved to transfer the lock - * @param _from address to transfer from - * @param _to address to transfer to - * @param _lockId id of lock to transfer - **/ - function transferFrom( - address _from, - address _to, - uint256 _lockId - ) external { - if (!_isApprovedOrOwner(msg.sender, _lockId)) revert SenderNotAuthorized(); - _transfer(_from, _to, _lockId); - } - - /** - * @notice transfers a lock between accounts and validates that the receiver supports ERC721 - * @dev - * - calls onERC721Received on `_to` if it is a contract or reverts if it is a contract - * and does not implemement onERC721Received - * - reverts if sender is not the owner of and not approved to transfer the lock - * - reverts if `_lockId` is invalid - * @param _from address to transfer from - * @param _to address to transfer to - * @param _lockId id of lock to transfer - **/ - function safeTransferFrom( - address _from, - address _to, - uint256 _lockId - ) external { - safeTransferFrom(_from, _to, _lockId, ""); + return lock; } - /** - * @notice transfers a lock between accounts and validates that the receiver supports ERC721 - * @dev - * - calls onERC721Received on `_to` if it is a contract or reverts if it is a contract - * and does not implemement onERC721Received - * - reverts if sender is not the owner of and not approved to transfer the lock - * - reverts if `_lockId` is invalid - * @param _from address to transfer from - * @param _to address to transfer to - * @param _lockId id of lock to transfer - * @param _data optional data to pass to receiver - **/ - function safeTransferFrom( - address _from, - address _to, + function handleIncomingRESDL( + address _receiver, uint256 _lockId, - bytes memory _data - ) public { - if (!_isApprovedOrOwner(msg.sender, _lockId)) revert SenderNotAuthorized(); - _transfer(_from, _to, _lockId); - if (!_checkOnERC721Received(_from, _to, _lockId, _data)) revert TransferToNonERC721Implementer(); - } - - /** - * @notice approves `_to` to transfer `_lockId` to another address - * @dev - * - approval is revoked on transfer and can also be revoked by approving zero address - * - reverts if sender is not owner of lock and not an approved operator for the owner - * - reverts if `_to` is owner of lock - * - reverts if `_lockId` is invalid - * @param _to address approved to transfer - * @param _lockId id of lock - **/ - function approve(address _to, uint256 _lockId) external { - address owner = ownerOf(_lockId); - - if (_to == owner) revert ApprovalToCurrentOwner(); - if (msg.sender != owner && !isApprovedForAll(owner, msg.sender)) revert SenderNotAuthorized(); - - tokenApprovals[_lockId] = _to; - emit Approval(owner, _to, _lockId); - } - - /** - * @notice returns the address approved to transfer a lock - * @param _lockId id of lock - * @return approved address - **/ - function getApproved(uint256 _lockId) public view returns (address) { - if (lockOwners[_lockId] == address(0)) revert InvalidLockId(); - - return tokenApprovals[_lockId]; - } - - /** - * @notice approves _operator to transfer all tokens owned by sender - * @dev - * - approval will not be revoked until this function is called again with - * `_approved` set to false - * - reverts if sender is `_operator` - * @param _operator address to approve/unapprove - * @param _approved whether address is approved or not - **/ - function setApprovalForAll(address _operator, bool _approved) external { - address owner = msg.sender; - if (owner == _operator) revert ApprovalToCaller(); - - operatorApprovals[owner][_operator] = _approved; - emit ApprovalForAll(owner, _operator, _approved); - } - - /** - * @notice returns whether `_operator` is approved to transfer all tokens owned by `_owner` - * @param _owner owner of tokens - * @param _operator address approved to transfer - * @return whether address is approved or not - **/ - function isApprovedForAll(address _owner, address _operator) public view returns (bool) { - return operatorApprovals[_owner][_operator]; - } + uint256 _amount, + uint256 _boostAmount, + uint64 _startTime, + uint64 _duration, + uint64 _expiry + ) external onlyCCIPController updateRewards(_receiver) updateRewards(ccipController) { + if (lockOwners[_lockId] != address(0)) revert InvalidLockId(); - /** - * @notice returns an account's staked amount for use by reward pools - * controlled by this contract - * @param _account account address - * @return account's staked amount - */ - function staked(address _account) external view override returns (uint256) { - return effectiveBalances[_account]; - } + locks[_lockId] = Lock(_amount, _boostAmount, _startTime, _duration, _expiry); + lockOwners[_lockId] = _receiver; + balances[_receiver] += 1; - /** - * @notice returns the total staked amount for use by reward pools - * controlled by this contract - * @return total staked amount - */ - function totalStaked() external view override returns (uint256) { - return totalEffectiveBalance; - } + uint256 totalAmount = _amount + _boostAmount; + effectiveBalances[_receiver] += totalAmount; + effectiveBalances[ccipController] -= totalAmount; - /** - * @notice returns whether this contract supports an interface - * @param _interfaceId id of interface - * @return whether contract supports interface or not - */ - function supportsInterface(bytes4 _interfaceId) external view returns (bool) { - return - _interfaceId == type(IERC721Upgradeable).interfaceId || - _interfaceId == type(IERC721MetadataUpgradeable).interfaceId || - _interfaceId == type(IERC165Upgradeable).interfaceId; + emit IncomingRESDL(_receiver, _lockId); } - /** - * @dev returns the URI for a token - */ - function tokenURI(uint256) external view returns (string memory) { - return baseURI; - } + function handleIncomingUpdate(uint256 _numNewRESDLTokens, int256 _totalRESDLSupplyChange) + external + onlyCCIPController + returns (uint256) + { + uint256 mintStartIndex; + if (_numNewRESDLTokens != 0) { + mintStartIndex = lastLockId + 1; + lastLockId += _numNewRESDLTokens; + } - /** - * @dev sets the base URI for all tokens - */ - function setBaseURI(string calldata _baseURI) external onlyOwner { - baseURI = _baseURI; - } + if (_totalRESDLSupplyChange > 0) { + totalEffectiveBalance += uint256(_totalRESDLSupplyChange); + } else if (_totalRESDLSupplyChange > 0) { + totalEffectiveBalance -= uint256(-1 * _totalRESDLSupplyChange); + } - /** - * @notice sets the boost controller - * @dev this contract handles boost calculations for locking SDL - * @param _boostController address of boost controller - */ - function setBoostController(address _boostController) external onlyOwner { - boostController = IBoostController(_boostController); - } + emit IncomingUpdate(_numNewRESDLTokens, _totalRESDLSupplyChange, mintStartIndex); - function setCCIPController(address _ccipController) external onlyOwner { - ccipController = _ccipController; + return mintStartIndex; } /** @@ -557,176 +252,63 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp ) external { if (msg.sender != delegatorPool) revert SenderNotAuthorized(); sdlToken.safeTransferFrom(delegatorPool, address(this), _amount); - _createLock(_sender, _amount, _lockingDuration); + _storeNewLock(_sender, _amount, _lockingDuration); } /** - * @notice creates a new lock - * @dev reverts if `_lockingDuration` exceeds maximum - * @param _sender owner of lock + * @notice stores a new lock + * @param _owner owner of lock * @param _amount amount to stake * @param _lockingDuration duration of lock */ - function _createLock( - address _sender, + function _storeNewLock( + address _owner, uint256 _amount, uint64 _lockingDuration - ) private updateRewards(_sender) { - uint256 boostAmount = boostController.getBoostAmount(_amount, _lockingDuration); - uint256 totalAmount = _amount + boostAmount; - uint64 startTime = _lockingDuration != 0 ? uint64(block.timestamp) : 0; + ) internal updateRewards(_owner) { + Lock memory lock = _createLock(_amount, _lockingDuration); uint256 lockId = lastLockId + 1; - locks[lockId] = Lock(_amount, boostAmount, startTime, _lockingDuration, 0); - lockOwners[lockId] = _sender; - balances[_sender] += 1; + locks[lockId] = lock; + lockOwners[lockId] = _owner; + balances[_owner] += 1; lastLockId++; - effectiveBalances[_sender] += totalAmount; + uint256 totalAmount = lock.amount + lock.boostAmount; + effectiveBalances[_owner] += totalAmount; totalEffectiveBalance += totalAmount; - emit CreateLock(_sender, lockId, _amount, boostAmount, _lockingDuration); - emit Transfer(address(0), _sender, lockId); + emit CreateLock(_owner, lockId, lock.amount, lock.boostAmount, lock.duration); + emit Transfer(address(0), _owner, lockId); } /** - * @notice updates an existing lock - * @dev - * - reverts if `_lockId` is invalid - * - reverts if `_lockingDuration` is less than current locking duration of lock - * - reverts if `_lockingDuration` exceeds maximum - * @param _sender owner of lock - * @param _lockId id of lock - * @param _amount additional amount to stake + * @notice stores an updated lock + * @param _owner owner of lock + * @param _amount amount to stake * @param _lockingDuration duration of lock */ - function _updateLock( - address _sender, + function _storeUpdatedLock( + address _owner, uint256 _lockId, uint256 _amount, uint64 _lockingDuration - ) private onlyLockOwner(_lockId, _sender) updateRewards(_sender) { - uint64 curLockingDuration = locks[_lockId].duration; - uint64 curExpiry = locks[_lockId].expiry; - if ((curExpiry == 0 || curExpiry > block.timestamp) && _lockingDuration < curLockingDuration) { - revert InvalidLockingDuration(); - } - - uint256 curBaseAmount = locks[_lockId].amount; + ) internal onlyLockOwner(_lockId, _owner) updateRewards(_owner) { + Lock memory lock = _updateLock(locks[_lockId], _amount, _lockingDuration); - uint256 baseAmount = curBaseAmount + _amount; - uint256 boostAmount = boostController.getBoostAmount(baseAmount, _lockingDuration); + int256 diffTotalAmount = int256(lock.amount + lock.boostAmount) - + int256(locks[_lockId].amount + locks[_lockId].boostAmount); - if (_amount != 0) { - locks[_lockId].amount = baseAmount; - } - - if (_lockingDuration != curLockingDuration) { - locks[_lockId].duration = _lockingDuration; - } - - if (_lockingDuration != 0) { - locks[_lockId].startTime = uint64(block.timestamp); - } else if (curLockingDuration != 0) { - delete locks[_lockId].startTime; - } - - if (locks[_lockId].expiry != 0) { - locks[_lockId].expiry = 0; - } - - int256 diffTotalAmount = int256(baseAmount + boostAmount) - int256(curBaseAmount + locks[_lockId].boostAmount); if (diffTotalAmount > 0) { - effectiveBalances[_sender] += uint256(diffTotalAmount); + effectiveBalances[_owner] += uint256(diffTotalAmount); totalEffectiveBalance += uint256(diffTotalAmount); } else if (diffTotalAmount < 0) { - effectiveBalances[_sender] -= uint256(-1 * diffTotalAmount); + effectiveBalances[_owner] -= uint256(-1 * diffTotalAmount); totalEffectiveBalance -= uint256(-1 * diffTotalAmount); } - locks[_lockId].boostAmount = boostAmount; + locks[_lockId] = lock; - emit UpdateLock(_sender, _lockId, baseAmount, boostAmount, _lockingDuration); - } - - /** - * @notice transfers a lock between accounts - * @dev - * - reverts if `_from` is not the owner of the lock - * - reverts if `to` is zero address - * @param _from address to transfer from - * @param _to address to transfer to - * @param _lockId id of lock to transfer - **/ - function _transfer( - address _from, - address _to, - uint256 _lockId - ) private { - if (_from != ownerOf(_lockId)) revert TransferFromIncorrectOwner(); - if (_to == address(0)) revert TransferToZeroAddress(); - - delete tokenApprovals[_lockId]; - - _updateRewards(_from); - _updateRewards(_to); - - uint256 effectiveBalanceChange = locks[_lockId].amount + locks[_lockId].boostAmount; - effectiveBalances[_from] -= effectiveBalanceChange; - effectiveBalances[_to] += effectiveBalanceChange; - - balances[_from] -= 1; - balances[_to] += 1; - lockOwners[_lockId] = _to; - - emit Transfer(_from, _to, _lockId); - } - - /** - * taken from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol - * @notice verifies that an address supports ERC721 and calls onERC721Received if applicable - * @dev - * - called after a lock is safe transferred - * - calls onERC721Received on `_to` if it is a contract or reverts if it is a contract - * and does not implemement onERC721Received - * @param _from address that lock is being transferred from - * @param _to address that lock is being transferred to - * @param _lockId id of lock - * @param _data optional data to be passed to receiver - */ - function _checkOnERC721Received( - address _from, - address _to, - uint256 _lockId, - bytes memory _data - ) private returns (bool) { - if (_to.code.length > 0) { - try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _lockId, _data) returns (bytes4 retval) { - return retval == IERC721Receiver.onERC721Received.selector; - } catch (bytes memory reason) { - if (reason.length == 0) { - revert TransferToNonERC721Implementer(); - } else { - assembly { - revert(add(32, reason), mload(reason)) - } - } - } - } else { - return true; - } - } - - /** - * @notice returns whether an account is authorized to transfer a lock - * @dev returns true if `_spender` is approved to transfer `_lockId` or if `_spender` is - * approved to transfer all locks owned by the owner of `_lockId` - * @param _spender address of account - * @param _lockId id of lock - * @return whether address is authorized ot not - **/ - function _isApprovedOrOwner(address _spender, uint256 _lockId) private view returns (bool) { - address owner = ownerOf(_lockId); - return (_spender == owner || isApprovedForAll(owner, _spender) || getApproved(_lockId) == _spender); + emit UpdateLock(_owner, _lockId, lock.amount, lock.boostAmount, lock.duration); } } diff --git a/contracts/core/sdlPool/SDLPoolSecondary.sol b/contracts/core/sdlPool/SDLPoolSecondary.sol new file mode 100644 index 00000000..7a4616cd --- /dev/null +++ b/contracts/core/sdlPool/SDLPoolSecondary.sol @@ -0,0 +1,424 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.15; + +import "./base/SDLPoolBase.sol"; + +/** + * @title SDL Pool Secondary + * @notice Allows users to stake/lock SDL tokens and receive a percentage of the protocol's earned rewards + * @dev deployed on all supported chains besides the primary chain + */ +contract SDLPoolSecondary is SDLPoolBase { + using SafeERC20Upgradeable for IERC20Upgradeable; + + struct NewLockPointer { + uint128 updateBatchIndex; + uint128 index; + } + struct LockUpdate { + uint128 updateBatchIndex; + Lock lock; + } + + mapping(uint256 => LockUpdate[]) internal queuedLockUpdates; + + uint256[] internal currentMintLockIdByBatch; + Lock[][] internal queuedNewLocks; + mapping(address => NewLockPointer[]) newLocksByOwner; + + uint128 internal updateBatchIndex; + uint64 internal updateInProgress; + uint64 internal updateNeeded; + int256 internal queuedRESDLSupplyChange; + + event QueueInitiateUnlock(address indexed owner, uint256 indexed lockId, uint64 expiry); + event QueueWithdraw(address indexed owner, uint256 indexed lockId, uint256 amount); + event QueueCreateLock(address indexed owner, uint256 amount, uint256 boostAmount, uint64 lockingDuration); + event QueueUpdateLock( + address indexed owner, + uint256 indexed lockId, + uint256 amount, + uint256 boostAmount, + uint64 lockingDuration + ); + event OutgoingUpdate(uint128 indexed batchIndex, uint256 numNewQueuedLocks, int256 reSDLSupplyChange); + event IncomingUpdate(uint128 indexed batchIndex, uint256 mintStartIndex); + + error CannotTransferWithQueuedUpdates(); + error UpdateInProgress(); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice initializes contract + * @param _name name of the staking derivative token + * @param _symbol symbol of the staking derivative token + * @param _sdlToken address of the SDL token + * @param _boostController address of the boost controller + **/ + function initialize( + string memory _name, + string memory _symbol, + address _sdlToken, + address _boostController + ) public initializer { + __SDLPoolBase_init(_name, _symbol, _sdlToken, _boostController); + updateBatchIndex = 1; + } + + /** + * @notice ERC677 implementation to stake/lock SDL tokens or distribute rewards + * @dev + * - will update/create a lock if the token transferred is SDL or will distribute rewards otherwise + * + * For Non-SDL: + * - reverts if token is unsupported + * + * For SDL: + * - set lockId to 0 to create a new lock or set lockId to > 0 to stake more into an existing lock + * - set lockingDuration to 0 to stake without locking or set lockingDuration to > 0 to lock for an amount + * time in seconds + * - see _updateLock() for more details on updating an existing lock or _createLock() for more details on + * creating a new lock + * @param _sender of the stake + * @param _value of the token transfer + * @param _calldata encoded lockId (uint256) and lockingDuration (uint64) + **/ + function onTokenTransfer( + address _sender, + uint256 _value, + bytes calldata _calldata + ) external override { + if (msg.sender != address(sdlToken) && !isTokenSupported(msg.sender)) revert UnauthorizedToken(); + + if (_value == 0) revert InvalidValue(); + + if (msg.sender == address(sdlToken)) { + (uint256 lockId, uint64 lockingDuration) = abi.decode(_calldata, (uint256, uint64)); + if (lockId != 0) { + _queueLockUpdate(_sender, lockId, _value, lockingDuration); + } else { + _queueNewLock(_sender, _value, lockingDuration); + } + } else { + distributeToken(msg.sender); + } + } + + /** + * @notice extends the locking duration of a lock + * @dev + * - reverts if `_lockId` is invalid or sender is not owner of lock + * - reverts if `_lockingDuration` is less than current locking duration of lock + * - reverts if `_lockingDuration` is 0 or exceeds the maximum + * @param _lockId id of lock + * @param _lockingDuration new locking duration to set + **/ + function extendLockDuration(uint256 _lockId, uint64 _lockingDuration) external { + if (_lockingDuration == 0) revert InvalidLockingDuration(); + _queueLockUpdate(msg.sender, _lockId, 0, _lockingDuration); + } + + /** + * @notice initiates the unlock period for a lock + * @dev + * - at least half of the locking duration must have elapsed to initiate the unlock period + * - the unlock period consists of half of the locking duration + * - boost will be set to 0 upon initiation of the unlock period + * + * - reverts if `_lockId` is invalid or sender is not owner of lock + * - reverts if a minimum of half the locking duration has not elapsed + * @param _lockId id of lock + **/ + function initiateUnlock(uint256 _lockId) external onlyLockOwner(_lockId, msg.sender) updateRewards(msg.sender) { + Lock memory lock = _getQueuedLockState(_lockId); + + if (lock.expiry != 0) revert UnlockAlreadyInitiated(); + uint64 halfDuration = lock.duration / 2; + if (lock.startTime + halfDuration > block.timestamp) revert HalfDurationNotElapsed(); + + uint64 expiry = uint64(block.timestamp) + halfDuration; + lock.expiry = expiry; + + uint256 boostAmount = lock.boostAmount; + lock.boostAmount = 0; + effectiveBalances[msg.sender] -= boostAmount; + totalEffectiveBalance -= boostAmount; + + queuedLockUpdates[_lockId].push(LockUpdate(updateBatchIndex, lock)); + queuedRESDLSupplyChange -= int256(boostAmount); + if (updateNeeded == 0) updateNeeded = 1; + + emit QueueInitiateUnlock(msg.sender, _lockId, expiry); + } + + /** + * @notice withdraws unlocked SDL + * @dev + * - SDL can only be withdrawn if unlocked (once the unlock period has elapsed or if it was never + * locked in the first place) + * - reverts if `_lockId` is invalid or sender is not owner of lock + * - reverts if not unlocked + * - reverts if `_amount` exceeds the amount staked in the lock + * @param _lockId id of the lock + * @param _amount amount to withdraw from the lock + **/ + function withdraw(uint256 _lockId, uint256 _amount) + external + onlyLockOwner(_lockId, msg.sender) + updateRewards(msg.sender) + { + Lock memory lock = _getQueuedLockState(_lockId); + + if (lock.startTime != 0) { + uint64 expiry = lock.expiry; + if (expiry == 0) revert UnlockNotInitiated(); + if (expiry > block.timestamp) revert TotalDurationNotElapsed(); + } + + uint256 baseAmount = lock.amount; + if (_amount > baseAmount) revert InsufficientBalance(); + + lock.amount = baseAmount - _amount; + effectiveBalances[msg.sender] -= _amount; + totalEffectiveBalance -= _amount; + + queuedLockUpdates[_lockId].push(LockUpdate(updateBatchIndex, lock)); + queuedRESDLSupplyChange -= int256(_amount); + if (updateNeeded == 0) updateNeeded = 1; + + emit QueueWithdraw(msg.sender, _lockId, _amount); + } + + function executeQueuedOperations(uint256[] memory _lockIds) external { + _executeQueuedLockUpdates(msg.sender, _lockIds); + _executeQueuedNewLocks(msg.sender); + } + + function handleOutgoingRESDL( + address _sender, + uint256 _lockId, + address _sdlReceiver + ) external onlyCCIPController onlyLockOwner(_lockId, _sender) updateRewards(_sender) returns (Lock memory) { + if (queuedLockUpdates[_lockId].length != 0) revert CannotTransferWithQueuedUpdates(); + + Lock memory lock = locks[_lockId]; + + delete locks[_lockId].amount; + delete lockOwners[_lockId]; + balances[_sender] -= 1; + + uint256 totalAmount = lock.amount + lock.boostAmount; + effectiveBalances[_sender] -= totalAmount; + totalEffectiveBalance -= totalAmount; + + sdlToken.safeTransfer(_sdlReceiver, lock.amount); + + emit OutgoingRESDL(_sender, _lockId); + + return lock; + } + + function handleIncomingRESDL( + address _receiver, + uint256 _lockId, + uint256 _amount, + uint256 _boostAmount, + uint64 _startTime, + uint64 _duration, + uint64 _expiry + ) external onlyCCIPController updateRewards(_receiver) { + if (lockOwners[_lockId] != address(0)) revert InvalidLockId(); + + locks[_lockId] = Lock(_amount, _boostAmount, _startTime, _duration, _expiry); + lockOwners[_lockId] = _receiver; + balances[_receiver] += 1; + + uint256 totalAmount = _amount + _boostAmount; + effectiveBalances[_receiver] += totalAmount; + totalEffectiveBalance += totalAmount; + + emit IncomingRESDL(_receiver, _lockId); + } + + function handleOutgoingUpdate() external onlyCCIPController returns (uint256, int256) { + if (updateInProgress == 1) revert UpdateInProgress(); + + uint256 numNewQueuedLocks = queuedNewLocks[updateBatchIndex].length; + int256 reSDLSupplyChange = queuedRESDLSupplyChange; + + queuedRESDLSupplyChange = 0; + updateBatchIndex++; + updateInProgress = 1; + updateNeeded = 0; + + emit OutgoingUpdate(updateBatchIndex - 1, numNewQueuedLocks, reSDLSupplyChange); + + return (numNewQueuedLocks, reSDLSupplyChange); + } + + function handleIncomingUpdate(uint256 _mintStartIndex) external onlyCCIPController { + currentMintLockIdByBatch[updateBatchIndex - 1] = _mintStartIndex; + updateInProgress = 0; + emit IncomingUpdate(updateBatchIndex - 1, _mintStartIndex); + } + + function shouldUpdate() external view returns (bool) { + return updateNeeded == 1 && updateInProgress == 0; + } + + function _queueNewLock( + address _owner, + uint256 _amount, + uint64 _lockingDuration + ) internal { + Lock memory lock = _createLock(_amount, _lockingDuration); + queuedNewLocks[updateBatchIndex].push(lock); + newLocksByOwner[_owner].push(NewLockPointer(updateBatchIndex, uint128(queuedNewLocks[updateBatchIndex].length - 1))); + queuedRESDLSupplyChange += int256(lock.amount + lock.boostAmount); + if (updateNeeded == 0) updateNeeded = 1; + + emit QueueCreateLock(_owner, _amount, lock.boostAmount, _lockingDuration); + } + + function _executeQueuedNewLocks(address _owner) internal updateRewards(_owner) { + uint128 finalizedBatchIndex = _getFinalizedUpdateBatchIndex(); + uint256 numNewLocks = newLocksByOwner[_owner].length; + uint256 i = 0; + while (i < numNewLocks) { + NewLockPointer memory newLockPointer = newLocksByOwner[_owner][i]; + if (newLockPointer.updateBatchIndex > finalizedBatchIndex) break; + + uint256 lockId = currentMintLockIdByBatch[newLockPointer.updateBatchIndex]; + Lock memory lock = queuedNewLocks[newLockPointer.updateBatchIndex][newLockPointer.index]; + + currentMintLockIdByBatch[newLockPointer.updateBatchIndex] += 1; + + locks[lockId] = lock; + lockOwners[lockId] = _owner; + balances[_owner] += 1; + + uint256 totalAmount = lock.amount + lock.boostAmount; + effectiveBalances[_owner] += totalAmount; + totalEffectiveBalance += totalAmount; + + emit CreateLock(_owner, lockId, lock.amount, lock.boostAmount, lock.duration); + emit Transfer(address(0), _owner, lockId); + + ++i; + } + + for (uint256 j = 0; j < numNewLocks; ++j) { + if (i == numNewLocks) { + delete newLocksByOwner[_owner][j]; + } else { + newLocksByOwner[_owner][j] = newLocksByOwner[_owner][i]; + ++i; + } + } + } + + function _queueLockUpdate( + address _owner, + uint256 _lockId, + uint256 _amount, + uint64 _lockingDuration + ) internal onlyLockOwner(_lockId, _owner) { + Lock memory lock = _getQueuedLockState(_lockId); + LockUpdate memory lockUpdate = LockUpdate(updateBatchIndex, _updateLock(lock, _amount, _lockingDuration)); + queuedLockUpdates[_lockId].push(lockUpdate); + queuedRESDLSupplyChange += + int256(lockUpdate.lock.amount + lockUpdate.lock.boostAmount) - + int256(lock.amount + lock.boostAmount); + if (updateNeeded == 0) updateNeeded = 1; + + emit QueueUpdateLock(_owner, _lockId, lockUpdate.lock.amount, lockUpdate.lock.boostAmount, lockUpdate.lock.duration); + } + + function _executeQueuedLockUpdates(address _owner, uint256[] memory _lockIds) internal updateRewards(_owner) { + uint128 finalizedBatchIndex = _getFinalizedUpdateBatchIndex(); + + for (uint256 i = 1; i < _lockIds.length; ++i) { + uint256 lockId = _lockIds[i]; + _onlyLockOwner(lockId, _owner); + uint256 numUpdates = queuedLockUpdates[lockId].length; + + Lock memory curLockState = locks[lockId]; + uint256 j = 0; + while (j < numUpdates) { + if (queuedLockUpdates[lockId][j].updateBatchIndex > finalizedBatchIndex) break; + + Lock memory updateLockState = queuedLockUpdates[lockId][j].lock; + int256 baseAmountDiff = int256(updateLockState.amount) - int256(curLockState.amount); + int256 boostAmountDiff = int256(updateLockState.boostAmount) - int256(curLockState.boostAmount); + + if (baseAmountDiff < 0) { + emit Withdraw(_owner, lockId, uint256(-1 * baseAmountDiff)); + if (updateLockState.amount == 0) { + delete locks[lockId]; + delete lockOwners[lockId]; + balances[_owner] -= 1; + if (tokenApprovals[lockId] != address(0)) delete tokenApprovals[lockId]; + emit Transfer(_owner, address(0), lockId); + } else { + locks[lockId].amount = updateLockState.amount; + } + sdlToken.safeTransfer(_owner, uint256(-1 * baseAmountDiff)); + } else if (boostAmountDiff < 0) { + locks[lockId].expiry = updateLockState.expiry; + locks[lockId].boostAmount = 0; + emit InitiateUnlock(_owner, lockId, updateLockState.expiry); + } else { + locks[lockId] = updateLockState; + uint256 totalDiff = uint256(baseAmountDiff + boostAmountDiff); + effectiveBalances[_owner] += totalDiff; + totalEffectiveBalance += totalDiff; + emit UpdateLock( + _owner, + lockId, + updateLockState.amount, + updateLockState.boostAmount, + updateLockState.duration + ); + } + curLockState = updateLockState; + ++j; + } + + for (uint256 k = 0; k < numUpdates; ++k) { + if (j == numUpdates) { + delete queuedLockUpdates[lockId][k]; + } else { + queuedLockUpdates[lockId][k] = queuedLockUpdates[lockId][j]; + ++j; + } + } + } + } + + function _getQueuedLockState(uint256 _lockId) internal view returns (Lock memory) { + uint256 updatesLength = queuedLockUpdates[_lockId].length; + + if (updatesLength != 0) { + return queuedLockUpdates[_lockId][updatesLength - 1].lock; + } else { + return locks[_lockId]; + } + } + + function _getFinalizedUpdateBatchIndex() internal view returns (uint128) { + return updateInProgress == 1 ? updateBatchIndex - 2 : updateBatchIndex - 1; + } + + function _transfer( + address _from, + address _to, + uint256 _lockId + ) internal override { + if (queuedLockUpdates[_lockId].length != 0) revert CannotTransferWithQueuedUpdates(); + super._transfer(_from, _to, _lockId); + } +} diff --git a/contracts/core/sdlPool/base/SDLPoolBase.sol b/contracts/core/sdlPool/base/SDLPoolBase.sol new file mode 100644 index 00000000..9c5f69b2 --- /dev/null +++ b/contracts/core/sdlPool/base/SDLPoolBase.sol @@ -0,0 +1,502 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.15; + +import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/IERC721MetadataUpgradeable.sol"; + +import "../../base/RewardsPoolController.sol"; +import "../../interfaces/IBoostController.sol"; +import "../../interfaces/IERC721Receiver.sol"; + +/** + * @title SDL Pool Base + * @notice Base SDL Pool contract to inherit from + */ +contract SDLPoolBase is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUpgradeable { + using SafeERC20Upgradeable for IERC20Upgradeable; + + struct Lock { + uint256 amount; + uint256 boostAmount; + uint64 startTime; + uint64 duration; + uint64 expiry; + } + + string public name; + string public symbol; + + mapping(address => mapping(address => bool)) internal operatorApprovals; + mapping(uint256 => address) internal tokenApprovals; + + IERC20Upgradeable public sdlToken; + IBoostController public boostController; + + uint256 public lastLockId; + mapping(uint256 => Lock) internal locks; + mapping(uint256 => address) internal lockOwners; + mapping(address => uint256) internal balances; + + uint256 public totalEffectiveBalance; + mapping(address => uint256) internal effectiveBalances; + + address public ccipController; + + string public baseURI; + + event InitiateUnlock(address indexed owner, uint256 indexed lockId, uint64 expiry); + event Withdraw(address indexed owner, uint256 indexed lockId, uint256 amount); + event CreateLock( + address indexed owner, + uint256 indexed lockId, + uint256 amount, + uint256 boostAmount, + uint64 lockingDuration + ); + event UpdateLock( + address indexed owner, + uint256 indexed lockId, + uint256 amount, + uint256 boostAmount, + uint64 lockingDuration + ); + event OutgoingRESDL(address indexed sender, uint256 indexed lockId); + event IncomingRESDL(address indexed receiver, uint256 indexed lockId); + + error SenderNotAuthorized(); + error InvalidLockId(); + error InvalidLockingDuration(); + error TransferFromIncorrectOwner(); + error TransferToZeroAddress(); + error TransferToNonERC721Implementer(); + error ApprovalToCurrentOwner(); + error ApprovalToCaller(); + error OnlyCCIPController(); + error InvalidValue(); + error InvalidParams(); + error UnauthorizedToken(); + error TotalDurationNotElapsed(); + error HalfDurationNotElapsed(); + error InsufficientBalance(); + error UnlockNotInitiated(); + error DuplicateContract(); + error ContractNotFound(); + error UnlockAlreadyInitiated(); + + modifier onlyCCIPController() { + if (msg.sender != ccipController) revert OnlyCCIPController(); + _; + } + + /** + * @notice initializes contract + * @param _name name of the staking derivative token + * @param _symbol symbol of the staking derivative token + * @param _sdlToken address of the SDL token + * @param _boostController address of the boost controller + **/ + function __SDLPoolBase_init( + string memory _name, + string memory _symbol, + address _sdlToken, + address _boostController + ) public onlyInitializing { + __RewardsPoolController_init(); + name = _name; + symbol = _symbol; + sdlToken = IERC20Upgradeable(_sdlToken); + boostController = IBoostController(_boostController); + } + + /** + * @notice reverts if `_owner` is not the owner of `_lockId` + **/ + modifier onlyLockOwner(uint256 _lockId, address _owner) { + _onlyLockOwner(_lockId, _owner); + _; + } + + /** + * @notice returns the effective stake balance of an account + * @dev the effective stake balance includes the actual amount of tokens an + * account has staked across all locks plus any applicable boost gained by locking + * @param _account address of account + * @return effective stake balance + **/ + function effectiveBalanceOf(address _account) external view returns (uint256) { + return effectiveBalances[_account]; + } + + /** + * @notice returns the number of locks owned by an account + * @param _account address of account + * @return total number of locks owned by account + **/ + function balanceOf(address _account) public view returns (uint256) { + return balances[_account]; + } + + /** + * @notice returns the owner of a lock + * @dev reverts if `_lockId` is invalid + * @param _lockId id of the lock + * @return lock owner + **/ + function ownerOf(uint256 _lockId) public view returns (address) { + address owner = lockOwners[_lockId]; + if (owner == address(0)) revert InvalidLockId(); + return owner; + } + + /** + * @notice returns the list of locks that corresponds to `_lockIds` + * @dev reverts if any lockId is invalid + * @param _lockIds list of lock ids + * @return list of locks + **/ + function getLocks(uint256[] calldata _lockIds) external view returns (Lock[] memory) { + Lock[] memory retLocks = new Lock[](_lockIds.length); + + for (uint256 i = 0; i < _lockIds.length; ++i) { + uint256 lockId = _lockIds[i]; + if (lockOwners[lockId] == address(0)) revert InvalidLockId(); + retLocks[i] = locks[lockId]; + } + + return retLocks; + } + + /** + * @notice returns a list of lockIds owned by an account + * @param _owner address of account + * @return list of lockIds + **/ + function getLockIdsByOwner(address _owner) external view returns (uint256[] memory) { + uint256 maxLockId = lastLockId; + uint256 lockCount = balanceOf(_owner); + uint256 lockIdsFound; + uint256[] memory lockIds = new uint256[](lockCount); + + for (uint256 i = 1; i <= maxLockId; ++i) { + if (lockOwners[i] == _owner) { + lockIds[lockIdsFound] = i; + lockIdsFound++; + if (lockIdsFound == lockCount) break; + } + } + + assert(lockIdsFound == lockCount); + + return lockIds; + } + + /** + * @notice transfers a lock between accounts + * @dev reverts if sender is not the owner of and not approved to transfer the lock + * @param _from address to transfer from + * @param _to address to transfer to + * @param _lockId id of lock to transfer + **/ + function transferFrom( + address _from, + address _to, + uint256 _lockId + ) external { + if (!_isApprovedOrOwner(msg.sender, _lockId)) revert SenderNotAuthorized(); + _transfer(_from, _to, _lockId); + } + + /** + * @notice transfers a lock between accounts and validates that the receiver supports ERC721 + * @dev + * - calls onERC721Received on `_to` if it is a contract or reverts if it is a contract + * and does not implemement onERC721Received + * - reverts if sender is not the owner of and not approved to transfer the lock + * - reverts if `_lockId` is invalid + * @param _from address to transfer from + * @param _to address to transfer to + * @param _lockId id of lock to transfer + **/ + function safeTransferFrom( + address _from, + address _to, + uint256 _lockId + ) external { + safeTransferFrom(_from, _to, _lockId, ""); + } + + /** + * @notice transfers a lock between accounts and validates that the receiver supports ERC721 + * @dev + * - calls onERC721Received on `_to` if it is a contract or reverts if it is a contract + * and does not implemement onERC721Received + * - reverts if sender is not the owner of and not approved to transfer the lock + * - reverts if `_lockId` is invalid + * @param _from address to transfer from + * @param _to address to transfer to + * @param _lockId id of lock to transfer + * @param _data optional data to pass to receiver + **/ + function safeTransferFrom( + address _from, + address _to, + uint256 _lockId, + bytes memory _data + ) public { + if (!_isApprovedOrOwner(msg.sender, _lockId)) revert SenderNotAuthorized(); + _transfer(_from, _to, _lockId); + if (!_checkOnERC721Received(_from, _to, _lockId, _data)) revert TransferToNonERC721Implementer(); + } + + /** + * @notice approves `_to` to transfer `_lockId` to another address + * @dev + * - approval is revoked on transfer and can also be revoked by approving zero address + * - reverts if sender is not owner of lock and not an approved operator for the owner + * - reverts if `_to` is owner of lock + * - reverts if `_lockId` is invalid + * @param _to address approved to transfer + * @param _lockId id of lock + **/ + function approve(address _to, uint256 _lockId) external { + address owner = ownerOf(_lockId); + + if (_to == owner) revert ApprovalToCurrentOwner(); + if (msg.sender != owner && !isApprovedForAll(owner, msg.sender)) revert SenderNotAuthorized(); + + tokenApprovals[_lockId] = _to; + emit Approval(owner, _to, _lockId); + } + + /** + * @notice returns the address approved to transfer a lock + * @param _lockId id of lock + * @return approved address + **/ + function getApproved(uint256 _lockId) public view returns (address) { + if (lockOwners[_lockId] == address(0)) revert InvalidLockId(); + + return tokenApprovals[_lockId]; + } + + /** + * @notice approves _operator to transfer all tokens owned by sender + * @dev + * - approval will not be revoked until this function is called again with + * `_approved` set to false + * - reverts if sender is `_operator` + * @param _operator address to approve/unapprove + * @param _approved whether address is approved or not + **/ + function setApprovalForAll(address _operator, bool _approved) external { + address owner = msg.sender; + if (owner == _operator) revert ApprovalToCaller(); + + operatorApprovals[owner][_operator] = _approved; + emit ApprovalForAll(owner, _operator, _approved); + } + + /** + * @notice returns whether `_operator` is approved to transfer all tokens owned by `_owner` + * @param _owner owner of tokens + * @param _operator address approved to transfer + * @return whether address is approved or not + **/ + function isApprovedForAll(address _owner, address _operator) public view returns (bool) { + return operatorApprovals[_owner][_operator]; + } + + /** + * @notice returns an account's staked amount for use by reward pools + * controlled by this contract + * @param _account account address + * @return account's staked amount + */ + function staked(address _account) external view override returns (uint256) { + return effectiveBalances[_account]; + } + + /** + * @notice returns the total staked amount for use by reward pools + * controlled by this contract + * @return total staked amount + */ + function totalStaked() external view override returns (uint256) { + return totalEffectiveBalance; + } + + /** + * @notice returns whether this contract supports an interface + * @param _interfaceId id of interface + * @return whether contract supports interface or not + */ + function supportsInterface(bytes4 _interfaceId) external view returns (bool) { + return + _interfaceId == type(IERC721Upgradeable).interfaceId || + _interfaceId == type(IERC721MetadataUpgradeable).interfaceId || + _interfaceId == type(IERC165Upgradeable).interfaceId; + } + + /** + * @dev returns the URI for a token + */ + function tokenURI(uint256) external view returns (string memory) { + return baseURI; + } + + /** + * @dev sets the base URI for all tokens + */ + function setBaseURI(string calldata _baseURI) external onlyOwner { + baseURI = _baseURI; + } + + /** + * @notice sets the boost controller + * @dev this contract handles boost calculations for locking SDL + * @param _boostController address of boost controller + */ + function setBoostController(address _boostController) external onlyOwner { + boostController = IBoostController(_boostController); + } + + function setCCIPController(address _ccipController) external onlyOwner { + ccipController = _ccipController; + } + + /** + * @notice creates a new lock + * @dev reverts if `_lockingDuration` exceeds maximum + * @param _amount amount to stake + * @param _lockingDuration duration of lock + */ + function _createLock(uint256 _amount, uint64 _lockingDuration) internal view returns (Lock memory) { + uint256 boostAmount = boostController.getBoostAmount(_amount, _lockingDuration); + uint64 startTime = _lockingDuration != 0 ? uint64(block.timestamp) : 0; + + return Lock(_amount, boostAmount, startTime, _lockingDuration, 0); + } + + /** + * @notice updates an existing lock + * @dev + * - reverts if `_lockId` is invalid + * - reverts if `_lockingDuration` is less than current locking duration of lock + * - reverts if `_lockingDuration` exceeds maximum + * @param _lock lock to update + * @param _amount additional amount to stake + * @param _lockingDuration duration of lock + */ + function _updateLock( + Lock memory _lock, + uint256 _amount, + uint64 _lockingDuration + ) internal view returns (Lock memory) { + if ((_lock.expiry == 0 || _lock.expiry > block.timestamp) && _lockingDuration < _lock.duration) { + revert InvalidLockingDuration(); + } + + Lock memory lock = Lock(_lock.amount, _lock.boostAmount, _lock.startTime, _lock.duration, _lock.expiry); + + uint256 baseAmount = _lock.amount + _amount; + uint256 boostAmount = boostController.getBoostAmount(baseAmount, _lockingDuration); + + if (_lockingDuration != 0) { + lock.startTime = uint64(block.timestamp); + } else { + delete lock.startTime; + } + + lock.amount = baseAmount; + lock.boostAmount = boostAmount; + lock.duration = _lockingDuration; + lock.expiry = 0; + + return lock; + } + + function _onlyLockOwner(uint256 _lockId, address _owner) internal view { + if (_owner != ownerOf(_lockId)) revert SenderNotAuthorized(); + } + + /** + * @notice transfers a lock between accounts + * @dev + * - reverts if `_from` is not the owner of the lock + * - reverts if `to` is zero address + * @param _from address to transfer from + * @param _to address to transfer to + * @param _lockId id of lock to transfer + **/ + function _transfer( + address _from, + address _to, + uint256 _lockId + ) internal virtual { + if (_from != ownerOf(_lockId)) revert TransferFromIncorrectOwner(); + if (_to == address(0)) revert TransferToZeroAddress(); + + delete tokenApprovals[_lockId]; + + _updateRewards(_from); + _updateRewards(_to); + + uint256 effectiveBalanceChange = locks[_lockId].amount + locks[_lockId].boostAmount; + effectiveBalances[_from] -= effectiveBalanceChange; + effectiveBalances[_to] += effectiveBalanceChange; + + balances[_from] -= 1; + balances[_to] += 1; + lockOwners[_lockId] = _to; + + emit Transfer(_from, _to, _lockId); + } + + /** + * taken from https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol + * @notice verifies that an address supports ERC721 and calls onERC721Received if applicable + * @dev + * - called after a lock is safe transferred + * - calls onERC721Received on `_to` if it is a contract or reverts if it is a contract + * and does not implemement onERC721Received + * @param _from address that lock is being transferred from + * @param _to address that lock is being transferred to + * @param _lockId id of lock + * @param _data optional data to be passed to receiver + */ + function _checkOnERC721Received( + address _from, + address _to, + uint256 _lockId, + bytes memory _data + ) internal returns (bool) { + if (_to.code.length > 0) { + try IERC721Receiver(_to).onERC721Received(msg.sender, _from, _lockId, _data) returns (bytes4 retval) { + return retval == IERC721Receiver.onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert TransferToNonERC721Implementer(); + } else { + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } else { + return true; + } + } + + /** + * @notice returns whether an account is authorized to transfer a lock + * @dev returns true if `_spender` is approved to transfer `_lockId` or if `_spender` is + * approved to transfer all locks owned by the owner of `_lockId` + * @param _spender address of account + * @param _lockId id of lock + * @return whether address is authorized ot not + **/ + function _isApprovedOrOwner(address _spender, uint256 _lockId) internal view returns (bool) { + address owner = ownerOf(_lockId); + return (_spender == owner || isApprovedForAll(owner, _spender) || getApproved(_lockId) == _spender); + } +} From b7dbba1bffd40e87db989284df2a8b00bdf18ccc Mon Sep 17 00:00:00 2001 From: BkChoy Date: Fri, 3 Nov 2023 15:37:26 -0400 Subject: [PATCH 10/42] sdl pool ccip controllers --- .../ccip/SDLPoolCCIPControllerPrimary.sol | 281 ++++++++++++++++++ .../ccip/SDLPoolCCIPControllerSecondary.sol | 162 ++++++++++ .../core/ccip/base/SDLPoolCCIPController.sol | 98 ++++++ .../interfaces/IRewardsPoolController.sol | 4 + contracts/core/interfaces/ISDLPool.sol | 10 +- .../core/test/SDLPoolCCIPControllerMock.sol | 4 +- 6 files changed, 554 insertions(+), 5 deletions(-) create mode 100644 contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol create mode 100644 contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol create mode 100644 contracts/core/ccip/base/SDLPoolCCIPController.sol diff --git a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol new file mode 100644 index 00000000..afb78aa9 --- /dev/null +++ b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import "./base/SDLPoolCCIPController.sol"; +import "../interfaces/ISDLPool.sol"; +import "../interfaces/IERC677.sol"; + +interface ISDLPoolPrimary is ISDLPool { + function handleIncomingUpdate(uint256 _numNewRESDLTokens, int256 _totalRESDLSupplyChange) external returns (uint256); +} + +contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { + using SafeERC20 for IERC20; + + uint64[] internal whitelistedChains; + mapping(uint64 => address) public whitelistedDestinations; + mapping(uint64 => bytes) public extraArgsByChain; + + mapping(uint64 => uint256) public reSDLSupplyByChain; + + mapping(address => address) public wrappedRewardTokens; + + event DistributeRewards(bytes32 indexed messageId, uint64 indexed destinationChainSelector, uint256 fees); + event ChainAdded(uint64 indexed chainSelector, address destination, bytes extraArgs); + event ChainRemoved(uint64 indexed destinationChainSelector, address destination); + event SetExtraArgs(uint64 indexed chainSelector, bytes extraArgs); + + /** + * @notice Initializes the contractMessageSent + * @param _router address of the CCIP router + * @param _linkToken address of the LINK token + * @param _sdlToken address of the SDL token + * @param _sdlPool address of the SDL Pool + **/ + constructor( + address _router, + address _linkToken, + address _sdlToken, + address _sdlPool + ) SDLPoolCCIPController(_router, _linkToken, _sdlToken, _sdlPool) {} + + function distributeRewards(bytes[] memory _extraArgs) external { + uint256 totalRESDL = ISDLPoolPrimary(sdlPool).effectiveBalanceOf(address(this)); + address[] memory tokens = ISDLPoolPrimary(sdlPool).supportedTokens(); + uint256 numDestinations = whitelistedChains.length; + + ISDLPoolPrimary(sdlPool).withdrawRewards(tokens); + + uint256[][] memory distributionAmounts = new uint256[][](numDestinations); + for (uint256 i = 0; i < numDestinations; ++i) { + distributionAmounts[i] = new uint256[](tokens.length); + } + + for (uint256 i = 0; i < tokens.length; ++i) { + address token = tokens[i]; + uint256 tokenBalance = IERC20(token).balanceOf(address(this)); + + address wrappedToken = wrappedRewardTokens[token]; + if (wrappedToken != address(0)) { + IERC677(token).transferAndCall(wrappedToken, tokenBalance, ""); + token = wrappedToken; + tokenBalance = IERC20(wrappedToken).balanceOf(address(this)); + } + + uint256 totalDistributed; + for (uint256 j = 0; j < numDestinations; ++j) { + uint64 chainSelector = whitelistedChains[j]; + uint256 rewards = j == numDestinations - 1 + ? tokenBalance - totalDistributed + : (tokenBalance * reSDLSupplyByChain[chainSelector]) / totalRESDL; + distributionAmounts[j][i] = rewards; + totalDistributed += rewards; + } + } + + for (uint256 i = 0; i < numDestinations; ++i) { + _distributeRewards(whitelistedChains[i], _extraArgs[i], tokens, distributionAmounts[i]); + } + } + + function handleOutgoingRESDL( + uint64 _destinationChainSelector, + address _sender, + uint256 _tokenId + ) + external + onlyBridge + returns ( + uint256, + uint256, + uint64, + uint64, + uint64 + ) + { + (uint256 amount, uint256 boostAmount, uint64 startTime, uint64 duration, uint64 expiry) = ISDLPoolPrimary(sdlPool) + .handleOutgoingRESDL(_sender, _tokenId, reSDLTokenBridge); + reSDLSupplyByChain[_destinationChainSelector] += amount + boostAmount; + return (amount, boostAmount, startTime, duration, expiry); + } + + function handleIncomingRESDL( + uint64 _sourceChainSelector, + address _receiver, + uint256 _tokenId, + uint256 _amount, + uint256 _boostAmount, + uint64 _startTime, + uint64 _duration, + uint64 _expiry + ) external onlyBridge { + sdlToken.safeTransferFrom(reSDLTokenBridge, sdlPool, _amount); + ISDLPoolPrimary(sdlPool).handleIncomingRESDL( + _receiver, + _tokenId, + _amount, + _boostAmount, + _startTime, + _duration, + _expiry + ); + reSDLSupplyByChain[_sourceChainSelector] -= _amount + _boostAmount; + } + + function getWhitelistedChains() external view returns (uint64[] memory) { + return whitelistedChains; + } + + /** + * @notice Whitelists a new chain + * @param _chainSelector id of chain + * @param _destination address to receive CCIP messages on chain + * @param _extraArgs extraArgs for this destination as defined in CCIP docs + **/ + function addWhitelistedChain( + uint64 _chainSelector, + address _destination, + bytes calldata _extraArgs + ) external onlyOwner { + if (whitelistedDestinations[_chainSelector] != address(0)) revert AlreadyAdded(); + if (_destination == address(0)) revert InvalidDestination(); + whitelistedChains.push(_chainSelector); + whitelistedDestinations[_chainSelector] = _destination; + extraArgsByChain[_chainSelector] = _extraArgs; + emit ChainAdded(_chainSelector, _destination, _extraArgs); + } + + /** + * @notice Removes an existing chain + * @param _chainSelector id of chain + **/ + function removeWhitelistedChain(uint64 _chainSelector) external onlyOwner { + if (whitelistedDestinations[_chainSelector] == address(0)) revert InvalidDestination(); + emit ChainRemoved(_chainSelector, whitelistedDestinations[_chainSelector]); + + for (uint256 i = 0; i < whitelistedChains.length; ++i) { + if (whitelistedChains[i] == _chainSelector) { + whitelistedChains[i] = whitelistedChains[whitelistedChains.length - 1]; + whitelistedChains.pop(); + } + } + + delete whitelistedDestinations[_chainSelector]; + delete extraArgsByChain[_chainSelector]; + } + + function setExtraArgs(uint64 _chainSelector, bytes calldata _extraArgs) external onlyOwner { + if (whitelistedDestinations[_chainSelector] == address(0)) revert InvalidDestination(); + extraArgsByChain[_chainSelector] = _extraArgs; + emit SetExtraArgs(_chainSelector, _extraArgs); + } + + function _distributeRewards( + uint64 _destinationChainSelector, + bytes memory _extraArgs, + address[] memory _rewardTokens, + uint256[] memory _rewardTokenAmounts + ) internal { + address destination = whitelistedDestinations[_destinationChainSelector]; + if (destination == address(0)) revert InvalidDestination(); + + uint256 numRewardTokensToTransfer; + for (uint256 i = 0; i < _rewardTokens.length; ++i) { + if (_rewardTokenAmounts[i] != 0) { + numRewardTokensToTransfer++; + } + } + + if (numRewardTokensToTransfer == 0) return; + + address[] memory rewardTokens = new address[](numRewardTokensToTransfer); + uint256[] memory rewardTokenAmounts = new uint256[](numRewardTokensToTransfer); + uint256 tokensAdded; + for (uint256 i = 0; i < _rewardTokens.length; ++i) { + if (_rewardTokenAmounts[i] != 0) { + rewardTokens[tokensAdded] = _rewardTokens[i]; + rewardTokenAmounts[tokensAdded] = _rewardTokenAmounts[i]; + tokensAdded++; + } + } + + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + destination, + 0, + rewardTokens, + rewardTokenAmounts, + _extraArgs + ); + + IRouterClient router = IRouterClient(this.getRouter()); + uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage); + + if (fees > maxLINKFee) revert FeeExceedsLimit(fees); + bytes32 messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage); + + emit DistributeRewards(messageId, _destinationChainSelector, fees); + } + + function _ccipReceive(Client.Any2EVMMessage memory _any2EvmMessage) internal override { + address sender = abi.decode(_any2EvmMessage.sender, (address)); + uint64 sourceChainSelector = _any2EvmMessage.sourceChainSelector; + if (sender != whitelistedDestinations[sourceChainSelector]) revert SenderNotAuthorized(); + + (uint256 numNewRESDLTokens, int256 totalRESDLSupplyChange) = abi.decode(_any2EvmMessage.data, (uint256, int256)); + + if (totalRESDLSupplyChange > 0) { + reSDLSupplyByChain[sourceChainSelector] += uint256(totalRESDLSupplyChange); + } else if (totalRESDLSupplyChange > 0) { + reSDLSupplyByChain[sourceChainSelector] -= uint256(-1 * totalRESDLSupplyChange); + } + + uint256 mintStartIndex = ISDLPoolPrimary(sdlPool).handleIncomingUpdate(numNewRESDLTokens, totalRESDLSupplyChange); + + _ccipSendUpdate(sourceChainSelector, mintStartIndex); + + emit MessageReceived(_any2EvmMessage.messageId, sourceChainSelector); + } + + function _ccipSendUpdate(uint64 _destinationChainSelector, uint256 _mintStartIndex) internal { + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + whitelistedDestinations[_destinationChainSelector], + _mintStartIndex, + new address[](0), + new uint256[](0), + extraArgsByChain[_destinationChainSelector] + ); + + IRouterClient router = IRouterClient(this.getRouter()); + uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage); + + if (fees > maxLINKFee) revert FeeExceedsLimit(fees); + bytes32 messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage); + + emit MessageSent(messageId, _destinationChainSelector, fees); + } + + function _buildCCIPMessage( + address _destination, + uint256 _mintStartIndex, + address[] memory _tokens, + uint256[] memory _tokenAmounts, + bytes memory _extraArgs + ) internal view returns (Client.EVM2AnyMessage memory) { + bool isRewardDistribution = _tokens.length != 0; + + Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](_tokens.length); + for (uint256 i = 0; i < _tokenAmounts.length; ++i) { + tokenAmounts[i] = Client.EVMTokenAmount({token: _tokens[i], amount: _tokenAmounts[i]}); + } + + Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(_destination), + data: isRewardDistribution ? bytes("") : abi.encode(_mintStartIndex), + tokenAmounts: tokenAmounts, + extraArgs: _extraArgs, + feeToken: address(linkToken) + }); + + return evm2AnyMessage; + } +} diff --git a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol new file mode 100644 index 00000000..8cfaa04a --- /dev/null +++ b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import "./base/SDLPoolCCIPController.sol"; +import "../interfaces/ISDLPool.sol"; + +interface ISDLPoolSecondary is ISDLPool { + function handleOutgoingUpdate() external returns (uint256, int256); + + function handleIncomingUpdate(uint256 _mintStartIndex) external; + + function shouldUpdate() external view returns (bool); +} + +contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { + using SafeERC20 for IERC20; + + uint64 internal timeOfLastUpdate; + uint64 internal timeBetweenUpdates; + + uint64 internal primaryChainSelector; + address internal primaryChainDestination; + bytes internal extraArgs; + + event SetPrimaryChain(uint64 primaryChainSelector, address primaryChainDestination); + + error UpdateConditionsNotMet(); + + constructor( + address _router, + address _linkToken, + address _sdlToken, + address _sdlPool, + uint64 _primaryChainSelector, + address _primaryChainDestination, + bytes memory _extraArgs + ) SDLPoolCCIPController(_router, _linkToken, _sdlToken, _sdlPool) { + primaryChainSelector = _primaryChainSelector; + primaryChainDestination = _primaryChainDestination; + extraArgs = _extraArgs; + } + + function checkUpkeep(bytes calldata) external view returns (bool, bytes memory) { + if (ISDLPoolSecondary(sdlPool).shouldUpdate() && block.timestamp > timeOfLastUpdate + timeBetweenUpdates) { + return (true, "0x"); + } + + return (false, "0x"); + } + + function performUpkeep(bytes calldata) external { + if (!ISDLPoolSecondary(sdlPool).shouldUpdate() || block.timestamp <= timeOfLastUpdate + timeBetweenUpdates) + revert UpdateConditionsNotMet(); + + timeOfLastUpdate = uint64(block.timestamp); + _initiateUpdate(primaryChainSelector, primaryChainDestination, extraArgs); + } + + function handleOutgoingRESDL(address _sender, uint256 _tokenId) + external + onlyBridge + returns ( + uint256, + uint256, + uint64, + uint64, + uint64 + ) + { + return ISDLPoolSecondary(sdlPool).handleOutgoingRESDL(_sender, _tokenId, reSDLTokenBridge); + } + + function handleIncomingRESDL( + address _receiver, + uint256 _tokenId, + uint256 _amount, + uint256 _boostAmount, + uint64 _startTime, + uint64 _duration, + uint64 _expiry + ) external onlyBridge { + sdlToken.safeTransferFrom(reSDLTokenBridge, sdlPool, _amount); + ISDLPoolSecondary(sdlPool).handleIncomingRESDL( + _receiver, + _tokenId, + _amount, + _boostAmount, + _startTime, + _duration, + _expiry + ); + } + + function setPrimaryChain(uint64 _primaryChainSelector, address _primaryChainDestination) external onlyOwner { + primaryChainSelector = _primaryChainSelector; + primaryChainDestination = _primaryChainDestination; + emit SetPrimaryChain(_primaryChainSelector, _primaryChainDestination); + } + + function _initiateUpdate( + uint64 _destinationChainSelector, + address _destination, + bytes memory _extraArgs + ) internal { + (uint256 numNewRESDLTokens, int256 totalRESDLSupplyChange) = ISDLPoolSecondary(sdlPool).handleOutgoingUpdate(); + + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + _destination, + numNewRESDLTokens, + totalRESDLSupplyChange, + _extraArgs + ); + + IRouterClient router = IRouterClient(this.getRouter()); + uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage); + + if (fees > maxLINKFee) revert FeeExceedsLimit(fees); + bytes32 messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage); + + emit MessageSent(messageId, _destinationChainSelector, fees); + } + + function _ccipReceive(Client.Any2EVMMessage memory _any2EvmMessage) internal override { + address sender = abi.decode(_any2EvmMessage.sender, (address)); + uint64 sourceChainSelector = _any2EvmMessage.sourceChainSelector; + if (sourceChainSelector != primaryChainSelector || sender != primaryChainDestination) revert SenderNotAuthorized(); + + if (_any2EvmMessage.data.length == 0) { + uint256 numRewardTokens = _any2EvmMessage.destTokenAmounts.length; + address[] memory rewardTokens = new address[](numRewardTokens); + if (numRewardTokens != 0) { + for (uint256 i = 0; i < numRewardTokens; ++i) { + rewardTokens[i] = _any2EvmMessage.destTokenAmounts[i].token; + IERC20(rewardTokens[i]).safeTransfer(sdlPool, _any2EvmMessage.destTokenAmounts[i].amount); + } + ISDLPoolSecondary(sdlPool).distributeTokens(rewardTokens); + } + } else { + uint256 mintStartIndex = abi.decode(_any2EvmMessage.data, (uint256)); + ISDLPoolSecondary(sdlPool).handleIncomingUpdate(mintStartIndex); + } + + emit MessageReceived(_any2EvmMessage.messageId, sourceChainSelector); + } + + function _buildCCIPMessage( + address _destination, + uint256 _numNewRESDLTokens, + int256 _totalRESDLSupplyChange, + bytes memory _extraArgs + ) internal view returns (Client.EVM2AnyMessage memory) { + Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({ + receiver: abi.encode(_destination), + data: abi.encode(_numNewRESDLTokens, _totalRESDLSupplyChange), + tokenAmounts: new Client.EVMTokenAmount[](0), + extraArgs: _extraArgs, + feeToken: address(linkToken) + }); + + return evm2AnyMessage; + } +} diff --git a/contracts/core/ccip/base/SDLPoolCCIPController.sol b/contracts/core/ccip/base/SDLPoolCCIPController.sol new file mode 100644 index 00000000..bb554db6 --- /dev/null +++ b/contracts/core/ccip/base/SDLPoolCCIPController.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol"; +import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; +import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { + using SafeERC20 for IERC20; + + IERC20 linkToken; + + IERC20 public sdlToken; + address public sdlPool; + address public reSDLTokenBridge; + + uint256 public maxLINKFee; + + event MessageSent(bytes32 indexed messageId, uint64 indexed destinationChainSelector, uint256 fees); + event MessageReceived(bytes32 indexed messageId, uint64 indexed destinationChainSelector); + event MessageFailed(bytes32 indexed messageId, bytes error); + + error OnlySelf(); + error OnlyRESDLTokenBridge(); + error AlreadyAdded(); + error InvalidDestination(); + error SenderNotAuthorized(); + error FeeExceedsLimit(uint256 fee); + + modifier onlySelf() { + if (msg.sender != address(this)) revert OnlySelf(); + _; + } + + modifier onlyBridge() { + if (msg.sender != reSDLTokenBridge) revert OnlyRESDLTokenBridge(); + _; + } + + /** + * @notice Initializes the contract + * @param _router address of the CCIP router + * @param _linkToken address of the LINK token + * @param _sdlToken address of the SDL token + * @param _sdlPool address of the SDL Pool + **/ + constructor( + address _router, + address _linkToken, + address _sdlToken, + address _sdlPool + ) CCIPReceiver(_router) { + linkToken = IERC20(_linkToken); + sdlToken = IERC20(_sdlToken); + sdlPool = _sdlPool; + linkToken.approve(_router, type(uint256).max); + } + + /** + * @notice Recovers tokens that were accidentally sent to this contract + * @param _tokens list of tokens to recover + * @param _receiver address to receive recovered tokens + **/ + function recoverTokens(address[] calldata _tokens, address _receiver) external onlyOwner { + for (uint256 i = 0; i < _tokens.length; ++i) { + IERC20 tokenToTransfer = IERC20(_tokens[i]); + tokenToTransfer.safeTransfer(_receiver, tokenToTransfer.balanceOf(address(this))); + } + } + + function setMaxLINKFee(uint256 _maxLINKFee) external onlyOwner { + maxLINKFee = _maxLINKFee; + } + + function setRESDLTokenBridge(address _reSDLTokenBridge) external onlyOwner { + reSDLTokenBridge = _reSDLTokenBridge; + } + + /** + * @notice Called by the CCIP router to deliver a message + * @param _any2EvmMessage CCIP message + **/ + function ccipReceive(Client.Any2EVMMessage calldata _any2EvmMessage) external override onlyRouter { + try this.processMessage(_any2EvmMessage) {} catch (bytes memory err) { + emit MessageFailed(_any2EvmMessage.messageId, err); + } + } + + /** + * @notice Processes a received message + * @param _any2EvmMessage CCIP message + **/ + function processMessage(Client.Any2EVMMessage calldata _any2EvmMessage) external onlySelf { + _ccipReceive(_any2EvmMessage); + } +} diff --git a/contracts/core/interfaces/IRewardsPoolController.sol b/contracts/core/interfaces/IRewardsPoolController.sol index 3269845f..331b7e69 100644 --- a/contracts/core/interfaces/IRewardsPoolController.sol +++ b/contracts/core/interfaces/IRewardsPoolController.sol @@ -22,4 +22,8 @@ interface IRewardsPoolController { * @param _rewardsPool token rewards pool to add **/ function addToken(address _token, address _rewardsPool) external; + + function distributeTokens(address[] memory _tokens) external; + + function withdrawRewards(address[] memory _tokens) external view; } diff --git a/contracts/core/interfaces/ISDLPool.sol b/contracts/core/interfaces/ISDLPool.sol index 6f0259f6..c1a1de46 100644 --- a/contracts/core/interfaces/ISDLPool.sol +++ b/contracts/core/interfaces/ISDLPool.sol @@ -1,12 +1,16 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.15; -interface ISDLPool { +import "./IRewardsPoolController.sol"; + +interface ISDLPool is IRewardsPoolController { function effectiveBalanceOf(address _account) external view returns (uint256); function ownerOf(uint256 _lockId) external view returns (address); - function burn( + function supportedTokens() external view returns (address[] memory); + + function handleOutgoingRESDL( address _sender, uint256 _lockId, address _sdlReceiver @@ -20,7 +24,7 @@ interface ISDLPool { uint64 _expiry ); - function mint( + function handleIncomingRESDL( address _receiver, uint256 _lockId, uint256 _amount, diff --git a/contracts/core/test/SDLPoolCCIPControllerMock.sol b/contracts/core/test/SDLPoolCCIPControllerMock.sol index f4563793..ad834029 100644 --- a/contracts/core/test/SDLPoolCCIPControllerMock.sol +++ b/contracts/core/test/SDLPoolCCIPControllerMock.sol @@ -36,7 +36,7 @@ contract SDLPoolCCIPControllerMock { uint64 ) { - return sdlPool.burn(_sender, _tokenId, reSDLTokenBridge); + return sdlPool.handleOutgoingRESDL(_sender, _tokenId, reSDLTokenBridge); } function handleIncomingRESDL( @@ -49,7 +49,7 @@ contract SDLPoolCCIPControllerMock { uint64 _expiry ) external onlyBridge { sdlToken.safeTransferFrom(reSDLTokenBridge, address(sdlPool), _amount); - sdlPool.mint(_receiver, _tokenId, _amount, _boostAmount, _startTime, _duration, _expiry); + sdlPool.handleIncomingRESDL(_receiver, _tokenId, _amount, _boostAmount, _startTime, _duration, _expiry); } function setRESDLTokenBridge(address _reSDLTokenBridge) external { From 0d429b1d16f6eee7bcafeae9d5a83c3b9728a5d1 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Fri, 3 Nov 2023 15:47:01 -0400 Subject: [PATCH 11/42] renamed sdl pool --- contracts/core/sdlPool/{SDLPool.sol => SDLPoolPrimary.sol} | 4 ++-- contracts/core/sdlPool/base/{SDLPoolBase.sol => SDLPool.sol} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename contracts/core/sdlPool/{SDLPool.sol => SDLPoolPrimary.sol} (99%) rename contracts/core/sdlPool/base/{SDLPoolBase.sol => SDLPool.sol} (99%) diff --git a/contracts/core/sdlPool/SDLPool.sol b/contracts/core/sdlPool/SDLPoolPrimary.sol similarity index 99% rename from contracts/core/sdlPool/SDLPool.sol rename to contracts/core/sdlPool/SDLPoolPrimary.sol index 54ee74ed..c92a0c4c 100644 --- a/contracts/core/sdlPool/SDLPool.sol +++ b/contracts/core/sdlPool/SDLPoolPrimary.sol @@ -4,11 +4,11 @@ pragma solidity 0.8.15; import "./base/SDLPoolBase.sol"; /** - * @title SDL Pool + * @title SDL Pool Primary * @notice Allows users to stake/lock SDL tokens and receive a percentage of the protocol's earned rewards * @dev deployed only on the primary chain */ -contract SDLPool is SDLPoolBase { +contract SDLPoolPrimary is SDLPoolBase { using SafeERC20Upgradeable for IERC20Upgradeable; address public delegatorPool; diff --git a/contracts/core/sdlPool/base/SDLPoolBase.sol b/contracts/core/sdlPool/base/SDLPool.sol similarity index 99% rename from contracts/core/sdlPool/base/SDLPoolBase.sol rename to contracts/core/sdlPool/base/SDLPool.sol index 9c5f69b2..5c7b89ed 100644 --- a/contracts/core/sdlPool/base/SDLPoolBase.sol +++ b/contracts/core/sdlPool/base/SDLPool.sol @@ -9,10 +9,10 @@ import "../../interfaces/IBoostController.sol"; import "../../interfaces/IERC721Receiver.sol"; /** - * @title SDL Pool Base + * @title SDL Pool * @notice Base SDL Pool contract to inherit from */ -contract SDLPoolBase is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUpgradeable { +contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUpgradeable { using SafeERC20Upgradeable for IERC20Upgradeable; struct Lock { From 9e997caef024c8c398ed16d9d2b20c4f680fb5b9 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Fri, 10 Nov 2023 13:47:37 -0500 Subject: [PATCH 12/42] ccip enabled sdl pool tests --- contracts/core/sdlPool/SDLPoolPrimary.sol | 30 +- contracts/core/sdlPool/SDLPoolSecondary.sol | 163 +- contracts/core/sdlPool/base/SDLPool.sol | 24 +- ...-pool.test.ts => sdl-pool-primary.test.ts} | 269 ++- test/core/sdlPool/sdl-pool-secondary.test.ts | 1871 +++++++++++++++++ 5 files changed, 2235 insertions(+), 122 deletions(-) rename test/core/sdlPool/{sdl-pool.test.ts => sdl-pool-primary.test.ts} (83%) create mode 100644 test/core/sdlPool/sdl-pool-secondary.test.ts diff --git a/contracts/core/sdlPool/SDLPoolPrimary.sol b/contracts/core/sdlPool/SDLPoolPrimary.sol index c92a0c4c..6ac9bbf3 100644 --- a/contracts/core/sdlPool/SDLPoolPrimary.sol +++ b/contracts/core/sdlPool/SDLPoolPrimary.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.15; -import "./base/SDLPoolBase.sol"; +import "./base/SDLPool.sol"; /** * @title SDL Pool Primary * @notice Allows users to stake/lock SDL tokens and receive a percentage of the protocol's earned rewards * @dev deployed only on the primary chain */ -contract SDLPoolPrimary is SDLPoolBase { +contract SDLPoolPrimary is SDLPool { using SafeERC20Upgradeable for IERC20Upgradeable; address public delegatorPool; @@ -163,6 +163,12 @@ contract SDLPoolPrimary is SDLPoolBase { sdlToken.safeTransfer(msg.sender, _amount); } + /** + * @notice handles an outgoing transfer of an reSDL lock to another chain + * @param _sender sender of lock + * @param _lockId id of lock + * @param _sdlReceiver address to receive underlying SDL on this chain + */ function handleOutgoingRESDL( address _sender, uint256 _lockId, @@ -192,6 +198,16 @@ contract SDLPoolPrimary is SDLPoolBase { return lock; } + /** + * @notice handles an incoming transfer of an reSDL lock from another chain + * @param _receiver receiver of lock + * @param _lockId id of lock + * @param _amount amount of underlying SDL + * @param _boostAmount reSDL boost amount + * @param _startTime start time of lock + * @param _duration duration of lock + * @param _expiry expiry time of lock + */ function handleIncomingRESDL( address _receiver, uint256 _lockId, @@ -214,6 +230,12 @@ contract SDLPoolPrimary is SDLPoolBase { emit IncomingRESDL(_receiver, _lockId); } + /** + * @notice handles an incoming update from a secondary chain + * @dev updates the total reSDL supply and keeps reSDL lock ids consistent between chains + * @param _numNewRESDLTokens number of new reSDL locks to be minted on other chain + * @param _totalRESDLSupplyChange total reSDL supply change on other chain + */ function handleIncomingUpdate(uint256 _numNewRESDLTokens, int256 _totalRESDLSupplyChange) external onlyCCIPController @@ -226,8 +248,10 @@ contract SDLPoolPrimary is SDLPoolBase { } if (_totalRESDLSupplyChange > 0) { + effectiveBalances[ccipController] += uint256(_totalRESDLSupplyChange); totalEffectiveBalance += uint256(_totalRESDLSupplyChange); - } else if (_totalRESDLSupplyChange > 0) { + } else if (_totalRESDLSupplyChange < 0) { + effectiveBalances[ccipController] -= uint256(-1 * _totalRESDLSupplyChange); totalEffectiveBalance -= uint256(-1 * _totalRESDLSupplyChange); } diff --git a/contracts/core/sdlPool/SDLPoolSecondary.sol b/contracts/core/sdlPool/SDLPoolSecondary.sol index 7a4616cd..170de200 100644 --- a/contracts/core/sdlPool/SDLPoolSecondary.sol +++ b/contracts/core/sdlPool/SDLPoolSecondary.sol @@ -1,14 +1,14 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.15; -import "./base/SDLPoolBase.sol"; +import "./base/SDLPool.sol"; /** * @title SDL Pool Secondary * @notice Allows users to stake/lock SDL tokens and receive a percentage of the protocol's earned rewards * @dev deployed on all supported chains besides the primary chain */ -contract SDLPoolSecondary is SDLPoolBase { +contract SDLPoolSecondary is SDLPool { using SafeERC20Upgradeable for IERC20Upgradeable; struct NewLockPointer { @@ -24,12 +24,12 @@ contract SDLPoolSecondary is SDLPoolBase { uint256[] internal currentMintLockIdByBatch; Lock[][] internal queuedNewLocks; - mapping(address => NewLockPointer[]) newLocksByOwner; + mapping(address => NewLockPointer[]) internal newLocksByOwner; - uint128 internal updateBatchIndex; - uint64 internal updateInProgress; + uint128 public updateBatchIndex; + uint64 public updateInProgress; uint64 internal updateNeeded; - int256 internal queuedRESDLSupplyChange; + int256 public queuedRESDLSupplyChange; event QueueInitiateUnlock(address indexed owner, uint256 indexed lockId, uint64 expiry); event QueueWithdraw(address indexed owner, uint256 indexed lockId, uint256 amount); @@ -46,6 +46,7 @@ contract SDLPoolSecondary is SDLPoolBase { error CannotTransferWithQueuedUpdates(); error UpdateInProgress(); + error NoUpdateInProgress(); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -67,10 +68,48 @@ contract SDLPoolSecondary is SDLPoolBase { ) public initializer { __SDLPoolBase_init(_name, _symbol, _sdlToken, _boostController); updateBatchIndex = 1; + currentMintLockIdByBatch.push(0); + queuedNewLocks.push(); + queuedNewLocks.push(); + } + + /** + * @notice returns a list of queued new locks for an owner + * @param _owner owner of locks + * @return list of queued locks and corresponding batch indexes + **/ + function getQueuedNewLocksByOwner(address _owner) external view returns (Lock[] memory, uint256[] memory) { + uint256 numNewLocks = newLocksByOwner[_owner].length; + Lock[] memory newLocks = new Lock[](numNewLocks); + uint256[] memory batchIndexes = new uint256[](numNewLocks); + + for (uint256 i = 0; i < numNewLocks; ++i) { + NewLockPointer memory pointer = newLocksByOwner[_owner][i]; + newLocks[i] = queuedNewLocks[pointer.updateBatchIndex][pointer.index]; + batchIndexes[i] = pointer.updateBatchIndex; + } + + return (newLocks, batchIndexes); + } + + /** + * @notice returns queued lock updates for a list of lock ids + * @param _lockIds list of lock ids + * @return list of queued lock updates corresponding to each lock id + **/ + function getQueuedLockUpdates(uint256[] calldata _lockIds) external view returns (LockUpdate[][] memory) { + LockUpdate[][] memory updates = new LockUpdate[][](_lockIds.length); + + for (uint256 i = 0; i < _lockIds.length; ++i) { + updates[i] = queuedLockUpdates[_lockIds[i]]; + } + + return updates; } /** * @notice ERC677 implementation to stake/lock SDL tokens or distribute rewards + * @dev operations will be queued until the next update at which point the user can execute (excludes reward distribution) * @dev * - will update/create a lock if the token transferred is SDL or will distribute rewards otherwise * @@ -81,8 +120,6 @@ contract SDLPoolSecondary is SDLPoolBase { * - set lockId to 0 to create a new lock or set lockId to > 0 to stake more into an existing lock * - set lockingDuration to 0 to stake without locking or set lockingDuration to > 0 to lock for an amount * time in seconds - * - see _updateLock() for more details on updating an existing lock or _createLock() for more details on - * creating a new lock * @param _sender of the stake * @param _value of the token transfer * @param _calldata encoded lockId (uint256) and lockingDuration (uint64) @@ -110,6 +147,7 @@ contract SDLPoolSecondary is SDLPoolBase { /** * @notice extends the locking duration of a lock + * @dev operation will be queued until the next update at which point the user can execute * @dev * - reverts if `_lockId` is invalid or sender is not owner of lock * - reverts if `_lockingDuration` is less than current locking duration of lock @@ -124,6 +162,7 @@ contract SDLPoolSecondary is SDLPoolBase { /** * @notice initiates the unlock period for a lock + * @dev operation will be queued until the next update at which point the user can execute * @dev * - at least half of the locking duration must have elapsed to initiate the unlock period * - the unlock period consists of half of the locking duration @@ -157,6 +196,7 @@ contract SDLPoolSecondary is SDLPoolBase { /** * @notice withdraws unlocked SDL + * @dev operation will be queued until the next update at which point the user can execute * @dev * - SDL can only be withdrawn if unlocked (once the unlock period has elapsed or if it was never * locked in the first place) @@ -193,11 +233,24 @@ contract SDLPoolSecondary is SDLPoolBase { emit QueueWithdraw(msg.sender, _lockId, _amount); } + /** + * @notice executes queued operations for the sender + * @dev will mint new locks and update existing locks + * @dev an operation can only be executed once its encompassing batch is finalized + * @param _lockIds ids of locks to update + **/ function executeQueuedOperations(uint256[] memory _lockIds) external { _executeQueuedLockUpdates(msg.sender, _lockIds); - _executeQueuedNewLocks(msg.sender); + _mintQueuedNewLocks(msg.sender); } + /** + * @notice handles the outgoing transfer of an reSDL lock to another chain + * @param _sender sender of the transfer + * @param _lockId id of lock + * @param _sdlReceiver address to receive underlying SDL on this chain + * @return lock the lock being transferred + **/ function handleOutgoingRESDL( address _sender, uint256 _lockId, @@ -222,6 +275,16 @@ contract SDLPoolSecondary is SDLPoolBase { return lock; } + /** + * @notice handles the incoming transfer of an reSDL lock from another chain + * @param _receiver receiver of the transfer + * @param _lockId id of lock + * @param _amount amount of underlying SDL + * @param _boostAmount reSDL boost amount + * @param _startTime start time of the lock + * @param _duration duration of the lock + * @param _expiry expiry time of the lock + **/ function handleIncomingRESDL( address _receiver, uint256 _lockId, @@ -241,9 +304,15 @@ contract SDLPoolSecondary is SDLPoolBase { effectiveBalances[_receiver] += totalAmount; totalEffectiveBalance += totalAmount; + if (_lockId > lastLockId) lastLockId = _lockId; + emit IncomingRESDL(_receiver, _lockId); } + /** + * @notice handles an outgoing update to the primary chain + * @return the number of new locks to mint and the reSDL supply change since the last update + **/ function handleOutgoingUpdate() external onlyCCIPController returns (uint256, int256) { if (updateInProgress == 1) revert UpdateInProgress(); @@ -254,22 +323,46 @@ contract SDLPoolSecondary is SDLPoolBase { updateBatchIndex++; updateInProgress = 1; updateNeeded = 0; + queuedNewLocks.push(); emit OutgoingUpdate(updateBatchIndex - 1, numNewQueuedLocks, reSDLSupplyChange); return (numNewQueuedLocks, reSDLSupplyChange); } + /** + * @notice handles an incoming update from the primary chain + * @dev an outgoing update must be sent prior to receiving an incoming update + * @dev finalizes the most recent batch of operations + * @param _mintStartIndex start index to use for minting new locks in the lastest batch + **/ function handleIncomingUpdate(uint256 _mintStartIndex) external onlyCCIPController { - currentMintLockIdByBatch[updateBatchIndex - 1] = _mintStartIndex; + if (updateInProgress == 0) revert NoUpdateInProgress(); + + if (_mintStartIndex != 0) { + uint256 newLastLockId = _mintStartIndex + queuedNewLocks[updateBatchIndex - 1].length - 1; + if (newLastLockId > lastLockId) lastLockId = newLastLockId; + } + + currentMintLockIdByBatch.push(_mintStartIndex); updateInProgress = 0; emit IncomingUpdate(updateBatchIndex - 1, _mintStartIndex); } + /** + * @notice returns whether an update should be sent to the primary chain + * @return whether update should be sent + **/ function shouldUpdate() external view returns (bool) { return updateNeeded == 1 && updateInProgress == 0; } + /** + * @notice queues a new lock to be minted + * @param _owner owner of lock + * @param _amount amount of underlying SDL + * @param _lockingDuration locking duration + **/ function _queueNewLock( address _owner, uint256 _amount, @@ -284,8 +377,13 @@ contract SDLPoolSecondary is SDLPoolBase { emit QueueCreateLock(_owner, _amount, lock.boostAmount, _lockingDuration); } - function _executeQueuedNewLocks(address _owner) internal updateRewards(_owner) { - uint128 finalizedBatchIndex = _getFinalizedUpdateBatchIndex(); + /** + * @notice mints queued new locks for an owner + * @dev will only mint locks that are part of finalized batches + * @param _owner owner address + **/ + function _mintQueuedNewLocks(address _owner) internal updateRewards(_owner) { + uint256 finalizedBatchIndex = _getFinalizedUpdateBatchIndex(); uint256 numNewLocks = newLocksByOwner[_owner].length; uint256 i = 0; while (i < numNewLocks) { @@ -313,7 +411,7 @@ contract SDLPoolSecondary is SDLPoolBase { for (uint256 j = 0; j < numNewLocks; ++j) { if (i == numNewLocks) { - delete newLocksByOwner[_owner][j]; + newLocksByOwner[_owner].pop(); } else { newLocksByOwner[_owner][j] = newLocksByOwner[_owner][i]; ++i; @@ -321,6 +419,13 @@ contract SDLPoolSecondary is SDLPoolBase { } } + /** + * @notice queued an update for a lock + * @param _owner owner of lock + * @param _lockId id of lock + * @param _amount new amount of underlying SDL + * @param _lockingDuration new locking duration + **/ function _queueLockUpdate( address _owner, uint256 _lockId, @@ -338,10 +443,16 @@ contract SDLPoolSecondary is SDLPoolBase { emit QueueUpdateLock(_owner, _lockId, lockUpdate.lock.amount, lockUpdate.lock.boostAmount, lockUpdate.lock.duration); } + /** + * @notice executes a series of lock updates + * @dev will only update locks that are part of finalized batches + * @param _owner owner of locks + * @param _lockIds list of ids for locks to update + **/ function _executeQueuedLockUpdates(address _owner, uint256[] memory _lockIds) internal updateRewards(_owner) { - uint128 finalizedBatchIndex = _getFinalizedUpdateBatchIndex(); + uint256 finalizedBatchIndex = _getFinalizedUpdateBatchIndex(); - for (uint256 i = 1; i < _lockIds.length; ++i) { + for (uint256 i = 0; i < _lockIds.length; ++i) { uint256 lockId = _lockIds[i]; _onlyLockOwner(lockId, _owner); uint256 numUpdates = queuedLockUpdates[lockId].length; @@ -390,7 +501,7 @@ contract SDLPoolSecondary is SDLPoolBase { for (uint256 k = 0; k < numUpdates; ++k) { if (j == numUpdates) { - delete queuedLockUpdates[lockId][k]; + queuedLockUpdates[lockId].pop(); } else { queuedLockUpdates[lockId][k] = queuedLockUpdates[lockId][j]; ++j; @@ -399,6 +510,12 @@ contract SDLPoolSecondary is SDLPoolBase { } } + /** + * @notice returns the current state of a lock + * @dev will return the most recent queued update for a lock or the finalized state if there are no queued updates + * @param _lockId id of lock + * @return the current state of a lock + **/ function _getQueuedLockState(uint256 _lockId) internal view returns (Lock memory) { uint256 updatesLength = queuedLockUpdates[_lockId].length; @@ -409,10 +526,20 @@ contract SDLPoolSecondary is SDLPoolBase { } } - function _getFinalizedUpdateBatchIndex() internal view returns (uint128) { - return updateInProgress == 1 ? updateBatchIndex - 2 : updateBatchIndex - 1; + /** + * @notice returns the index of the latest finalized batch + * @return latest finalized batch index + **/ + function _getFinalizedUpdateBatchIndex() internal view returns (uint256) { + return currentMintLockIdByBatch.length - 1; } + /** + * @notice transfers a lock between accounts + * @param _from account to transfer from + * @param _to account to transfer to + * @param _lockId id of lock to tansfer + **/ function _transfer( address _from, address _to, diff --git a/contracts/core/sdlPool/base/SDLPool.sol b/contracts/core/sdlPool/base/SDLPool.sol index 5c7b89ed..679fe280 100644 --- a/contracts/core/sdlPool/base/SDLPool.sol +++ b/contracts/core/sdlPool/base/SDLPool.sol @@ -83,11 +83,6 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp error ContractNotFound(); error UnlockAlreadyInitiated(); - modifier onlyCCIPController() { - if (msg.sender != ccipController) revert OnlyCCIPController(); - _; - } - /** * @notice initializes contract * @param _name name of the staking derivative token @@ -116,6 +111,14 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp _; } + /** + * @notice reverts if sender is not the CCIP controller + **/ + modifier onlyCCIPController() { + if (msg.sender != ccipController) revert OnlyCCIPController(); + _; + } + /** * @notice returns the effective stake balance of an account * @dev the effective stake balance includes the actual amount of tokens an @@ -360,6 +363,11 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp boostController = IBoostController(_boostController); } + /** + * @notice sets the CCIP controller + * @dev this contract interfaces with CCIP + * @param _ccipController address of CCIP controller + */ function setCCIPController(address _ccipController) external onlyOwner { ccipController = _ccipController; } @@ -415,6 +423,12 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp return lock; } + /** + * @notice checks if a lock is owned by an certain account + * @dev reverts if lock is not owner by account + * @param _lockId id of lock + * @param _owner owner address + **/ function _onlyLockOwner(uint256 _lockId, address _owner) internal view { if (_owner != ownerOf(_lockId)) revert SenderNotAuthorized(); } diff --git a/test/core/sdlPool/sdl-pool.test.ts b/test/core/sdlPool/sdl-pool-primary.test.ts similarity index 83% rename from test/core/sdlPool/sdl-pool.test.ts rename to test/core/sdlPool/sdl-pool-primary.test.ts index 01503ae0..b4d68d5e 100644 --- a/test/core/sdlPool/sdl-pool.test.ts +++ b/test/core/sdlPool/sdl-pool-primary.test.ts @@ -9,11 +9,10 @@ import { deployUpgradeable, } from '../../utils/helpers' import { - DelegatorPool, ERC677, LinearBoostController, RewardsPool, - SDLPool, + SDLPoolPrimary, StakingAllowance, } from '../../../typechain-types' import { ethers } from 'hardhat' @@ -30,13 +29,12 @@ const parseLocks = (locks: any) => expiry: l.expiry.toNumber(), })) -describe('SDLPool', () => { +describe('SDLPoolPrimary', () => { let sdlToken: StakingAllowance let rewardToken: ERC677 let rewardsPool: RewardsPool let boostController: LinearBoostController - let sdlPool: SDLPool - let delegatorPool: DelegatorPool + let sdlPool: SDLPoolPrimary let signers: Signer[] let accounts: string[] @@ -51,25 +49,17 @@ describe('SDLPool', () => { await sdlToken.mint(accounts[0], toEther(1000000)) await setupToken(sdlToken, accounts) - delegatorPool = (await deployUpgradeable('DelegatorPool', [ - sdlToken.address, - 'Staked SDL', - 'stSDL', - [], - ])) as DelegatorPool - boostController = (await deploy('LinearBoostController', [ 4 * 365 * DAY, 4, ])) as LinearBoostController - sdlPool = (await deployUpgradeable('SDLPool', [ + sdlPool = (await deployUpgradeable('SDLPoolPrimary', [ 'Reward Escrowed SDL', 'reSDL', sdlToken.address, boostController.address, - delegatorPool.address, - ])) as SDLPool + ])) as SDLPoolPrimary rewardsPool = (await deploy('RewardsPool', [ sdlPool.address, @@ -77,6 +67,7 @@ describe('SDLPool', () => { ])) as RewardsPool await sdlPool.addToken(rewardToken.address, rewardsPool.address) + await sdlPool.setCCIPController(accounts[0]) }) it('token name and symbol should be correct', async () => { @@ -1113,94 +1104,180 @@ describe('SDLPool', () => { assert.equal(fromEther(await rewardsPool.userRewards(accounts[1])), 500) }) - it('migration from delegator pool should work correctly', async () => { - let dpRewardsPool = (await deploy('RewardsPool', [ - delegatorPool.address, - rewardToken.address, - ])) as RewardsPool + it('handleOutoingRESDL should work correctly', async () => { + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken + .connect(signers[2]) + .transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 20000]) + ) + let ts1 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await time.increase(20000) + await sdlPool.connect(signers[2]).initiateUnlock(2) + let ts2 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await sdlToken + .connect(signers[3]) + .transferAndCall( + sdlPool.address, + toEther(300), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * DAY]) + ) + let ts3 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp - await delegatorPool.addToken(rewardToken.address, dpRewardsPool.address) - - for (let i = 0; i < 2; i++) { - await sdlToken.connect(signers[i]).transferAndCall(delegatorPool.address, toEther(250), '0x') - assert.equal(fromEther(await delegatorPool.balanceOf(accounts[i])), 250) - assert.equal(fromEther(await delegatorPool.availableBalanceOf(accounts[i])), 250) - assert.equal(fromEther(await delegatorPool.lockedBalanceOf(accounts[i])), 0) - } - for (let i = 2; i < 4; i++) { - await sdlToken - .connect(signers[i]) - .transferAndCall( - delegatorPool.address, - toEther(1000), - ethers.utils.defaultAbiCoder.encode(['uint256'], [toEther(400)]) - ) - assert.equal(fromEther(await delegatorPool.balanceOf(accounts[i])), 1000) - assert.equal(fromEther(await delegatorPool.availableBalanceOf(accounts[i])), 600) - assert.equal(fromEther(await delegatorPool.lockedBalanceOf(accounts[i])), 400) - } - await rewardToken.transferAndCall(delegatorPool.address, toEther(1000), '0x') - await rewardToken.transfer(accounts[5], await rewardToken.balanceOf(accounts[0])) - assert.equal(fromEther(await sdlToken.balanceOf(delegatorPool.address)), 2500) - assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 0) + const startingEffectiveBalance = await sdlPool.totalEffectiveBalance() - await expect(delegatorPool.migrate(toEther(10), 0)).to.be.revertedWith( - 'Cannot migrate until contract is retired' + assert.deepEqual( + parseLocks([await sdlPool.callStatic.handleOutgoingRESDL(accounts[1], 1, accounts[4])])[0], + { amount: 100, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 } ) - await expect(sdlPool.migrate(accounts[2], toEther(100), 0)).to.be.revertedWith( - 'SenderNotAuthorized()' + await sdlPool.handleOutgoingRESDL(accounts[1], 1, accounts[4]) + await expect(sdlPool.ownerOf(1)).to.be.revertedWith('InvalidLockId()') + assert.equal(fromEther(await sdlToken.balanceOf(accounts[4])), 100) + assert.isTrue((await sdlPool.totalEffectiveBalance()).eq(startingEffectiveBalance)) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[1])), 0) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 100) + assert.equal((await sdlPool.balanceOf(accounts[1])).toNumber(), 0) + + assert.deepEqual( + parseLocks([await sdlPool.callStatic.handleOutgoingRESDL(accounts[2], 2, accounts[5])])[0], + { amount: 200, boostAmount: 0, startTime: ts1, duration: 20000, expiry: ts2 + 10000 } ) + await sdlPool.handleOutgoingRESDL(accounts[2], 2, accounts[5]) + await expect(sdlPool.ownerOf(2)).to.be.revertedWith('InvalidLockId()') + assert.equal(fromEther(await sdlToken.balanceOf(accounts[5])), 200) + assert.isTrue((await sdlPool.totalEffectiveBalance()).eq(startingEffectiveBalance)) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[2])), 0) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 300) + assert.equal((await sdlPool.balanceOf(accounts[2])).toNumber(), 0) - await delegatorPool.retireDelegatorPool([accounts[2], accounts[3]], sdlPool.address) - assert.equal(fromEther(await sdlToken.balanceOf(delegatorPool.address)), 500) - assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 1200) - assert.equal(fromEther(await delegatorPool.totalSupply()), 500) - for (let i = 2; i < 4; i++) { - assert.equal(fromEther(await delegatorPool.balanceOf(accounts[i])), 0) - assert.equal(fromEther(await delegatorPool.availableBalanceOf(accounts[i])), 0) - assert.equal(fromEther(await delegatorPool.lockedBalanceOf(accounts[i])), 0) - assert.equal(fromEther(await delegatorPool.approvedLockedBalanceOf(accounts[i])), 0) - assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[i])), 600) - assert.equal(fromEther(await rewardToken.balanceOf(accounts[i])), 400) - } - - await delegatorPool.migrate(toEther(100), 0) - assert.equal(fromEther(await sdlToken.balanceOf(delegatorPool.address)), 400) - assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 1300) - assert.equal(fromEther(await delegatorPool.totalSupply()), 400) - assert.equal(fromEther(await delegatorPool.balanceOf(accounts[0])), 150) - assert.equal(fromEther(await delegatorPool.availableBalanceOf(accounts[0])), 150) - assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 100) - assert.equal(fromEther(await rewardToken.balanceOf(accounts[0])), 100) + assert.deepEqual( + parseLocks([await sdlPool.callStatic.handleOutgoingRESDL(accounts[3], 3, accounts[6])])[0], + { amount: 300, boostAmount: 300, startTime: ts3, duration: 365 * DAY, expiry: 0 } + ) + await sdlPool.handleOutgoingRESDL(accounts[3], 3, accounts[6]) + await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') + assert.equal(fromEther(await sdlToken.balanceOf(accounts[6])), 300) + assert.isTrue((await sdlPool.totalEffectiveBalance()).eq(startingEffectiveBalance)) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[3])), 0) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 900) + assert.equal((await sdlPool.balanceOf(accounts[3])).toNumber(), 0) - await expect(delegatorPool.migrate(toEther(200), 0)).to.be.revertedWith('Insufficient balance') - await expect(delegatorPool.migrate(0, 0)).to.be.revertedWith('Invalid amount') + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await rewardToken.transferAndCall(sdlPool.address, toEther(10000), '0x') + + let rewards1 = await rewardsPool.withdrawableRewards(accounts[1]) + let rewards2 = await rewardsPool.withdrawableRewards(accounts[0]) + + await sdlPool.handleOutgoingRESDL(accounts[1], 4, accounts[2]) + + assert.isTrue((await rewardsPool.withdrawableRewards(accounts[1])).eq(rewards1)) + assert.isTrue((await rewardsPool.withdrawableRewards(accounts[0])).eq(rewards2)) + }) + + it('handleIncomingRESDL should work correctly', async () => { + await sdlToken.transferAndCall( + sdlPool.address, + toEther(1000), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await expect( + sdlPool.handleIncomingRESDL(accounts[1], 1, toEther(100), toEther(50), 123, 456, 789) + ).to.be.revertedWith('InvalidLockId()') + await sdlPool.handleOutgoingRESDL(accounts[0], 1, accounts[0]) + + const startingEffectiveBalance = await sdlPool.totalEffectiveBalance() + + await sdlPool.handleIncomingRESDL(accounts[1], 7, toEther(100), toEther(50), 123, 456, 0) + assert.deepEqual(parseLocks(await sdlPool.getLocks([7]))[0], { + amount: 100, + boostAmount: 50, + startTime: 123, + duration: 456, + expiry: 0, + }) + assert.isTrue((await sdlPool.totalEffectiveBalance()).eq(startingEffectiveBalance)) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[1])), 150) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 850) + assert.equal(await sdlPool.ownerOf(7), accounts[1]) + assert.equal((await sdlPool.balanceOf(accounts[1])).toNumber(), 1) - await delegatorPool.migrate(toEther(150), 0) - assert.equal(fromEther(await sdlToken.balanceOf(delegatorPool.address)), 250) - assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 1450) - assert.equal(fromEther(await delegatorPool.totalSupply()), 250) - assert.equal(fromEther(await delegatorPool.balanceOf(accounts[0])), 0) - assert.equal(fromEther(await delegatorPool.availableBalanceOf(accounts[0])), 0) + await sdlPool.handleIncomingRESDL(accounts[2], 9, toEther(200), toEther(400), 1, 2, 3) + assert.deepEqual(parseLocks(await sdlPool.getLocks([9]))[0], { + amount: 200, + boostAmount: 400, + startTime: 1, + duration: 2, + expiry: 3, + }) + assert.isTrue((await sdlPool.totalEffectiveBalance()).eq(startingEffectiveBalance)) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[2])), 600) assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 250) - assert.equal(fromEther(await rewardToken.balanceOf(accounts[0])), 100) - - await delegatorPool.connect(signers[1]).migrate(toEther(100), 365 * DAY) - assert.equal(fromEther(await sdlToken.balanceOf(delegatorPool.address)), 150) - assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 1550) - assert.equal(fromEther(await delegatorPool.totalSupply()), 150) - assert.equal(fromEther(await delegatorPool.balanceOf(accounts[1])), 150) - assert.equal(fromEther(await delegatorPool.availableBalanceOf(accounts[1])), 150) - assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[1])), 200) - assert.equal(fromEther(await rewardToken.balanceOf(accounts[1])), 100) - - await delegatorPool.connect(signers[1]).migrate(toEther(150), 2 * 365 * DAY) - assert.equal(fromEther(await sdlToken.balanceOf(delegatorPool.address)), 0) - assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 1700) - assert.equal(fromEther(await delegatorPool.totalSupply()), 0) - assert.equal(fromEther(await delegatorPool.balanceOf(accounts[1])), 0) - assert.equal(fromEther(await delegatorPool.availableBalanceOf(accounts[1])), 0) - assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[1])), 650) - assert.equal(fromEther(await rewardToken.balanceOf(accounts[1])), 100) + assert.equal(await sdlPool.ownerOf(9), accounts[2]) + assert.equal((await sdlPool.balanceOf(accounts[2])).toNumber(), 1) + + await rewardToken.transferAndCall(sdlPool.address, toEther(10000), '0x') + + let rewards1 = await rewardsPool.withdrawableRewards(accounts[3]) + let rewards2 = await rewardsPool.withdrawableRewards(accounts[0]) + + await sdlPool.handleIncomingRESDL(accounts[3], 10, toEther(50), toEther(100), 1, 2, 3) + + assert.isTrue((await rewardsPool.withdrawableRewards(accounts[3])).eq(rewards1)) + assert.isTrue((await rewardsPool.withdrawableRewards(accounts[0])).eq(rewards2)) + }) + + it('handleIncomingUpdate should work correctly', async () => { + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(1000), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(1000), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + + assert.equal((await sdlPool.callStatic.handleIncomingUpdate(5, toEther(2000))).toNumber(), 3) + await sdlPool.handleIncomingUpdate(5, toEther(2000)) + assert.equal((await sdlPool.lastLockId()).toNumber(), 7) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 4000) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 2000) + + assert.equal((await sdlPool.callStatic.handleIncomingUpdate(3, toEther(-500))).toNumber(), 8) + await sdlPool.handleIncomingUpdate(3, toEther(-500)) + assert.equal((await sdlPool.lastLockId()).toNumber(), 10) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 3500) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 1500) + + assert.equal((await sdlPool.callStatic.handleIncomingUpdate(0, toEther(100))).toNumber(), 0) + await sdlPool.handleIncomingUpdate(0, toEther(100)) + assert.equal((await sdlPool.lastLockId()).toNumber(), 10) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 3600) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 1600) + + assert.equal((await sdlPool.callStatic.handleIncomingUpdate(3, toEther(0))).toNumber(), 11) + await sdlPool.handleIncomingUpdate(3, toEther(0)) + assert.equal((await sdlPool.lastLockId()).toNumber(), 13) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 3600) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 1600) }) }) diff --git a/test/core/sdlPool/sdl-pool-secondary.test.ts b/test/core/sdlPool/sdl-pool-secondary.test.ts new file mode 100644 index 00000000..fb7d47a7 --- /dev/null +++ b/test/core/sdlPool/sdl-pool-secondary.test.ts @@ -0,0 +1,1871 @@ +import { Signer } from 'ethers' +import { assert, expect } from 'chai' +import { + toEther, + deploy, + getAccounts, + setupToken, + fromEther, + deployUpgradeable, +} from '../../utils/helpers' +import { + ERC677, + LinearBoostController, + RewardsPool, + SDLPoolSecondary, + StakingAllowance, +} from '../../../typechain-types' +import { ethers } from 'hardhat' +import { time } from '@nomicfoundation/hardhat-network-helpers' + +const DAY = 86400 + +const parseLock = (lock: any) => ({ + amount: fromEther(lock.amount), + boostAmount: Number(fromEther(lock.boostAmount).toFixed(4)), + startTime: lock.startTime.toNumber(), + duration: lock.duration.toNumber(), + expiry: lock.expiry.toNumber(), +}) + +const parseLocks = (locks: any) => locks.map((l: any) => parseLock(l)) + +const parseNewLocks = (locks: any) => [parseLocks(locks[0]), locks[1].map((v: any) => v.toNumber())] + +const parseLockUpdates = (locks: any) => + locks.map((lock: any) => + lock.map((update: any) => ({ + updateBatchIndex: update[0].toNumber(), + lock: parseLock(update.lock), + })) + ) + +describe('SDLPoolSecondary', () => { + let sdlToken: StakingAllowance + let rewardToken: ERC677 + let rewardsPool: RewardsPool + let boostController: LinearBoostController + let sdlPool: SDLPoolSecondary + let signers: Signer[] + let accounts: string[] + + const mintLock = async (lock = true, signerIndex = 0) => { + await sdlToken + .connect(signers[signerIndex]) + .transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, lock ? 365 * DAY : 0]) + ) + let ts = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await sdlPool.handleOutgoingUpdate() + await sdlPool.handleIncomingUpdate(1) + await sdlPool.executeQueuedOperations([]) + return ts + } + + const updateLocks = async (mintIndex = 0, ids = [1]) => { + await sdlPool.handleOutgoingUpdate() + await sdlPool.handleIncomingUpdate(mintIndex) + await sdlPool.executeQueuedOperations(ids) + } + + before(async () => { + ;({ signers, accounts } = await getAccounts()) + }) + + beforeEach(async () => { + sdlToken = (await deploy('StakingAllowance', ['stake.link', 'SDL'])) as StakingAllowance + rewardToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + + await sdlToken.mint(accounts[0], toEther(1000000)) + await setupToken(sdlToken, accounts) + + boostController = (await deploy('LinearBoostController', [ + 4 * 365 * DAY, + 4, + ])) as LinearBoostController + + sdlPool = (await deployUpgradeable('SDLPoolSecondary', [ + 'Reward Escrowed SDL', + 'reSDL', + sdlToken.address, + boostController.address, + ])) as SDLPoolSecondary + + rewardsPool = (await deploy('RewardsPool', [ + sdlPool.address, + rewardToken.address, + ])) as RewardsPool + + await sdlPool.addToken(rewardToken.address, rewardsPool.address) + await sdlPool.setCCIPController(accounts[0]) + }) + + it('token name and symbol should be correct', async () => { + assert.equal(await sdlPool.name(), 'Reward Escrowed SDL') + assert.equal(await sdlPool.symbol(), 'reSDL') + }) + + it('should be able to stake without locking', async () => { + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken + .connect(signers[2]) + .transferAndCall( + sdlPool.address, + toEther(300), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(400), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 0) + assert.equal(fromEther(await sdlPool.totalStaked()), 0) + assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 1000) + + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[0])), [ + [ + { amount: 100, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 400, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + ], + [1, 1], + ]) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[1])), [ + [{ amount: 200, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }], + [1], + ]) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[2])), [ + [{ amount: 300, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }], + [1], + ]) + + await sdlPool.handleOutgoingUpdate() + await sdlPool.handleIncomingUpdate(1) + await sdlPool.executeQueuedOperations([]) + await sdlPool.connect(signers[1]).executeQueuedOperations([]) + await sdlPool.connect(signers[2]).executeQueuedOperations([]) + + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 1000) + assert.equal(fromEther(await sdlPool.totalStaked()), 1000) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1, 2, 3, 4])), [ + { amount: 100, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 400, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 200, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 300, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + ]) + + assert.equal(await sdlPool.ownerOf(1), accounts[0]) + assert.equal(await sdlPool.ownerOf(2), accounts[0]) + assert.equal(await sdlPool.ownerOf(3), accounts[1]) + assert.equal(await sdlPool.ownerOf(4), accounts[2]) + + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[0])), [[], []]) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[1])), [[], []]) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[2])), [[], []]) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 500) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 500) + assert.equal((await sdlPool.lastLockId()).toNumber(), 4) + assert.equal((await sdlPool.balanceOf(accounts[0])).toNumber(), 2) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[0])).map((v) => v.toNumber()), + [1, 2] + ) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[1])), 200) + assert.equal(fromEther(await sdlPool.staked(accounts[1])), 200) + assert.equal((await sdlPool.balanceOf(accounts[1])).toNumber(), 1) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[1])).map((v) => v.toNumber()), + [3] + ) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[2])), 300) + assert.equal(fromEther(await sdlPool.staked(accounts[2])), 300) + assert.equal((await sdlPool.balanceOf(accounts[2])).toNumber(), 1) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[2])).map((v) => v.toNumber()), + [4] + ) + }) + + it('should be able to stake with locking', async () => { + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * DAY]) + ) + let ts1 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 4 * 365 * DAY]) + ) + let ts2 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await sdlToken + .connect(signers[2]) + .transferAndCall( + sdlPool.address, + toEther(300), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 100 * DAY]) + ) + let ts3 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await sdlToken.transferAndCall( + sdlPool.address, + toEther(400), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + + assert.equal(Number(fromEther(await sdlPool.queuedRESDLSupplyChange()).toFixed(4)), 1982.1918) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 0) + assert.equal(fromEther(await sdlPool.totalStaked()), 0) + assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 1000) + + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[0])), [ + [ + { amount: 100, boostAmount: 100, startTime: ts1, duration: 365 * DAY, expiry: 0 }, + { amount: 400, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + ], + [1, 1], + ]) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[1])), [ + [{ amount: 200, boostAmount: 800, startTime: ts2, duration: 4 * 365 * DAY, expiry: 0 }], + [1], + ]) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[2])), [ + [{ amount: 300, boostAmount: 82.1918, startTime: ts3, duration: 100 * DAY, expiry: 0 }], + [1], + ]) + + await sdlPool.handleOutgoingUpdate() + await sdlPool.handleIncomingUpdate(1) + await sdlPool.executeQueuedOperations([]) + await sdlPool.connect(signers[1]).executeQueuedOperations([]) + await sdlPool.connect(signers[2]).executeQueuedOperations([]) + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0) + assert.equal(Number(fromEther(await sdlPool.totalEffectiveBalance()).toFixed(4)), 1982.1918) + assert.equal(Number(fromEther(await sdlPool.totalStaked()).toFixed(4)), 1982.1918) + assert.equal((await sdlPool.lastLockId()).toNumber(), 4) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1, 2, 3, 4])), [ + { amount: 100, boostAmount: 100, startTime: ts1, duration: 365 * DAY, expiry: 0 }, + { amount: 400, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 200, boostAmount: 800, startTime: ts2, duration: 4 * 365 * DAY, expiry: 0 }, + { amount: 300, boostAmount: 82.1918, startTime: ts3, duration: 100 * DAY, expiry: 0 }, + ]) + + assert.equal(await sdlPool.ownerOf(1), accounts[0]) + assert.equal(await sdlPool.ownerOf(2), accounts[0]) + assert.equal(await sdlPool.ownerOf(3), accounts[1]) + assert.equal(await sdlPool.ownerOf(4), accounts[2]) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 600) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 600) + assert.equal((await sdlPool.balanceOf(accounts[0])).toNumber(), 2) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[0])).map((v) => v.toNumber()), + [1, 2] + ) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[1])), 1000) + assert.equal(fromEther(await sdlPool.staked(accounts[1])), 1000) + assert.equal((await sdlPool.balanceOf(accounts[1])).toNumber(), 1) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[1])).map((v) => v.toNumber()), + [3] + ) + + assert.equal( + Number(fromEther(await sdlPool.effectiveBalanceOf(accounts[2])).toFixed(4)), + 382.1918 + ) + assert.equal(Number(fromEther(await sdlPool.staked(accounts[2])).toFixed(4)), 382.1918) + assert.equal((await sdlPool.balanceOf(accounts[2])).toNumber(), 1) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[2])).map((v) => v.toNumber()), + [4] + ) + }) + + it('should be able to lock an existing stake', async () => { + await mintLock(false) + + await sdlPool.extendLockDuration(1, 365 * DAY) + let ts = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + + assert.deepEqual(parseLockUpdates(await sdlPool.getQueuedLockUpdates([1])), [ + [ + { + updateBatchIndex: 2, + lock: { amount: 100, boostAmount: 100, startTime: ts, duration: 365 * DAY, expiry: 0 }, + }, + ], + ]) + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 100) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 100) + assert.equal(fromEther(await sdlPool.totalStaked()), 100) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 100) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 100) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { amount: 100, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + ]) + await expect(sdlPool.extendLockDuration(1, 100)).to.be.revertedWith('InvalidLockingDuration()') + + await updateLocks() + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 200) + assert.equal(fromEther(await sdlPool.totalStaked()), 200) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 200) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 200) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { amount: 100, boostAmount: 100, startTime: ts, duration: 365 * DAY, expiry: 0 }, + ]) + await expect(sdlPool.extendLockDuration(1, 100)).to.be.revertedWith('InvalidLockingDuration()') + }) + + it('should be able extend the duration of a lock', async () => { + let ts1 = await mintLock() + + await sdlPool.extendLockDuration(1, 2 * 365 * DAY) + let ts2 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + + assert.deepEqual(parseLockUpdates(await sdlPool.getQueuedLockUpdates([1])), [ + [ + { + updateBatchIndex: 2, + lock: { + amount: 100, + boostAmount: 200, + startTime: ts2, + duration: 2 * 365 * DAY, + expiry: 0, + }, + }, + ], + ]) + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 100) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 200) + assert.equal(fromEther(await sdlPool.totalStaked()), 200) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 200) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 200) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { amount: 100, boostAmount: 100, startTime: ts1, duration: 365 * DAY, expiry: 0 }, + ]) + await expect(sdlPool.extendLockDuration(1, 365 * DAY)).to.be.revertedWith( + 'InvalidLockingDuration()' + ) + + await updateLocks() + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 300) + assert.equal(fromEther(await sdlPool.totalStaked()), 300) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 300) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 300) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { amount: 100, boostAmount: 200, startTime: ts2, duration: 2 * 365 * DAY, expiry: 0 }, + ]) + await expect(sdlPool.extendLockDuration(1, 365 * DAY)).to.be.revertedWith( + 'InvalidLockingDuration()' + ) + }) + + it('should be able add more stake without locking', async () => { + await mintLock(false) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 0]) + ) + + assert.deepEqual(parseLockUpdates(await sdlPool.getQueuedLockUpdates([1])), [ + [ + { + updateBatchIndex: 2, + lock: { + amount: 300, + boostAmount: 0, + startTime: 0, + duration: 0, + expiry: 0, + }, + }, + ], + ]) + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 200) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 100) + assert.equal(fromEther(await sdlPool.totalStaked()), 100) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 100) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 100) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { amount: 100, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + ]) + + await updateLocks() + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 300) + assert.equal(fromEther(await sdlPool.totalStaked()), 300) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 300) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 300) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { amount: 300, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + ]) + }) + + it('should be able to add more stake to a lock with and without extending the duration', async () => { + let ts0 = await mintLock() + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 365 * DAY]) + ) + let ts1 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + + assert.deepEqual(parseLockUpdates(await sdlPool.getQueuedLockUpdates([1])), [ + [ + { + updateBatchIndex: 2, + lock: { amount: 300, boostAmount: 300, startTime: ts1, duration: 365 * DAY, expiry: 0 }, + }, + ], + ]) + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 400) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 200) + assert.equal(fromEther(await sdlPool.totalStaked()), 200) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 200) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 200) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { amount: 100, boostAmount: 100, startTime: ts0, duration: 365 * DAY, expiry: 0 }, + ]) + + await updateLocks() + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 600) + assert.equal(fromEther(await sdlPool.totalStaked()), 600) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 600) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 600) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { amount: 300, boostAmount: 300, startTime: ts1, duration: 365 * DAY, expiry: 0 }, + ]) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 2 * 365 * DAY]) + ) + let ts2 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + + assert.deepEqual(parseLockUpdates(await sdlPool.getQueuedLockUpdates([1])), [ + [ + { + updateBatchIndex: 3, + lock: { + amount: 500, + boostAmount: 1000, + startTime: ts2, + duration: 2 * 365 * DAY, + expiry: 0, + }, + }, + ], + ]) + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 900) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 600) + assert.equal(fromEther(await sdlPool.totalStaked()), 600) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 600) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 600) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { amount: 300, boostAmount: 300, startTime: ts1, duration: 365 * DAY, expiry: 0 }, + ]) + + await updateLocks() + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 1500) + assert.equal(fromEther(await sdlPool.totalStaked()), 1500) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 1500) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 1500) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { amount: 500, boostAmount: 1000, startTime: ts2, duration: 2 * 365 * DAY, expiry: 0 }, + ]) + }) + + it('should not be able to stake 0 when creating or updating a lock', async () => { + await expect( + sdlToken.transferAndCall( + sdlPool.address, + 0, + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + ).to.be.revertedWith('InvalidValue()') + await expect( + sdlToken.transferAndCall( + sdlPool.address, + 0, + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * DAY]) + ) + ).to.be.revertedWith('InvalidValue()') + + await mintLock(false) + await expect( + sdlToken.transferAndCall( + sdlPool.address, + 0, + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 0]) + ) + ).to.be.revertedWith('InvalidValue()') + await expect( + sdlToken.transferAndCall( + sdlPool.address, + 0, + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 365 * DAY]) + ) + ).to.be.revertedWith('InvalidValue()') + }) + + it('should not be able to decrease the duration of a lock', async () => { + await mintLock() + + await expect( + sdlToken.transferAndCall( + sdlPool.address, + toEther(10), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 365 * DAY - 1]) + ) + ).to.be.revertedWith('InvalidLockingDuration()') + await expect(sdlPool.extendLockDuration(1, 365 * DAY - 1)).to.be.revertedWith( + 'InvalidLockingDuration()' + ) + }) + + it('should not be able to extend the duration of a lock to 0', async () => { + await mintLock() + await expect(sdlPool.extendLockDuration(1, 0)).to.be.revertedWith('InvalidLockingDuration()') + }) + + it('only the lock owner should be able to update a lock, lock id must be valid', async () => { + await mintLock() + + await expect(sdlPool.connect(signers[1]).extendLockDuration(1, 366 * DAY)).to.be.revertedWith( + 'SenderNotAuthorized()' + ) + await expect(sdlPool.extendLockDuration(2, 366 * DAY)).to.be.revertedWith('InvalidLockId()') + await expect( + sdlToken + .connect(signers[2]) + .transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 365 * DAY]) + ) + ).to.be.revertedWith('SenderNotAuthorized()') + await expect( + sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [2, 365 * DAY]) + ) + ).to.be.revertedWith('InvalidLockId()') + }) + + it('should be able to initiate an unlock', async () => { + let ts1 = await mintLock() + await time.increase(200 * DAY) + + await sdlPool.initiateUnlock(1) + let ts2 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + + assert.deepEqual(parseLockUpdates(await sdlPool.getQueuedLockUpdates([1])), [ + [ + { + updateBatchIndex: 2, + lock: { + amount: 100, + boostAmount: 0, + startTime: ts1, + duration: 365 * DAY, + expiry: ts2 + (365 / 2) * DAY, + }, + }, + ], + ]) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { + amount: 100, + boostAmount: 100, + startTime: ts1, + duration: 365 * DAY, + expiry: 0, + }, + ]) + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), -100) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 100) + assert.equal(fromEther(await sdlPool.totalStaked()), 100) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 100) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 100) + + await updateLocks() + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 100) + assert.equal(fromEther(await sdlPool.totalStaked()), 100) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 100) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 100) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { + amount: 100, + boostAmount: 0, + startTime: ts1, + duration: 365 * DAY, + expiry: ts2 + (365 / 2) * DAY, + }, + ]) + }) + + it('should not be able to initiate unlock until half of locking period has elapsed', async () => { + let ts = await mintLock() + + await time.setNextBlockTimestamp(ts + (365 / 2) * DAY - 1) + await expect(sdlPool.initiateUnlock(1)).to.be.revertedWith('HalfDurationNotElapsed()') + await time.setNextBlockTimestamp(ts + (365 / 2) * DAY) + await sdlPool.initiateUnlock(1) + }) + + it('only the lock owner should be able to initiate an unlock, lock id must be valid', async () => { + await mintLock() + await time.increase(365 * DAY) + await expect(sdlPool.connect(signers[1]).initiateUnlock(1)).to.be.revertedWith( + 'SenderNotAuthorized()' + ) + await expect(sdlPool.initiateUnlock(2)).to.be.revertedWith('InvalidLockId()') + }) + + it('should be able to update a lock after an unlock has been initiated', async () => { + await mintLock() + await time.increase(200 * DAY) + await sdlPool.initiateUnlock(1) + await updateLocks() + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 365 * DAY]) + ) + let ts = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await updateLocks() + + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 400) + assert.equal(fromEther(await sdlPool.totalStaked()), 400) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 400) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 400) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { + amount: 200, + boostAmount: 200, + startTime: ts, + duration: 365 * DAY, + expiry: 0, + }, + ]) + + await time.increase(200 * DAY) + await sdlPool.initiateUnlock(1) + await sdlPool.extendLockDuration(1, 2 * 365 * DAY) + ts = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await updateLocks() + + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 600) + assert.equal(fromEther(await sdlPool.totalStaked()), 600) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 600) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 600) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { + amount: 200, + boostAmount: 400, + startTime: ts, + duration: 2 * 365 * DAY, + expiry: 0, + }, + ]) + }) + + it('should be able to update a lock after a lock has been fully unlocked', async () => { + await mintLock() + await time.increase(200 * DAY) + await sdlPool.initiateUnlock(1) + await time.increase(200 * DAY) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 0]) + ) + await updateLocks() + + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 200) + assert.equal(fromEther(await sdlPool.totalStaked()), 200) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 200) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 200) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { + amount: 200, + boostAmount: 0, + startTime: 0, + duration: 0, + expiry: 0, + }, + ]) + }) + + it('should be able to withdraw and burn lock NFT', async () => { + let ts1 = await mintLock() + await time.increase(200 * DAY) + await sdlPool.initiateUnlock(1) + let ts2 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await time.increase(200 * DAY) + + let startingBalance = await sdlToken.balanceOf(accounts[0]) + await sdlPool.withdraw(1, toEther(20)) + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), -120) + + await updateLocks() + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0) + assert.equal(fromEther((await sdlToken.balanceOf(accounts[0])).sub(startingBalance)), 20) + assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 80) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 80) + assert.equal(fromEther(await sdlPool.totalStaked()), 80) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 80) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 80) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { + amount: 80, + boostAmount: 0, + startTime: ts1, + duration: 365 * DAY, + expiry: ts2 + (365 / 2) * DAY, + }, + ]) + + startingBalance = await sdlToken.balanceOf(accounts[0]) + await sdlPool.withdraw(1, toEther(80)) + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), -80) + + await updateLocks() + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0) + assert.equal(fromEther((await sdlToken.balanceOf(accounts[0])).sub(startingBalance)), 80) + assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 0) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 0) + assert.equal(fromEther(await sdlPool.totalStaked()), 0) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 0) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 0) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[0])).map((v) => v.toNumber()), + [] + ) + assert.equal((await sdlPool.balanceOf(accounts[0])).toNumber(), 0) + await expect(sdlPool.ownerOf(1)).to.be.revertedWith('InvalidLockId()') + }) + + it('should be able withdraw tokens that were never locked', async () => { + await mintLock(false) + + let startingBalance = await sdlToken.balanceOf(accounts[0]) + await sdlPool.withdraw(1, toEther(20)) + await updateLocks() + + assert.equal(fromEther((await sdlToken.balanceOf(accounts[0])).sub(startingBalance)), 20) + assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 80) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 80) + assert.equal(fromEther(await sdlPool.totalStaked()), 80) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 80) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 80) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1])), [ + { + amount: 80, + boostAmount: 0, + startTime: 0, + duration: 0, + expiry: 0, + }, + ]) + + startingBalance = await sdlToken.balanceOf(accounts[0]) + await sdlPool.withdraw(1, toEther(80)) + await updateLocks() + + assert.equal(fromEther((await sdlToken.balanceOf(accounts[0])).sub(startingBalance)), 80) + assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 0) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 0) + assert.equal(fromEther(await sdlPool.totalStaked()), 0) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 0) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 0) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[0])).map((v) => v.toNumber()), + [] + ) + assert.equal((await sdlPool.balanceOf(accounts[0])).toNumber(), 0) + await expect(sdlPool.ownerOf(1)).to.be.revertedWith('InvalidLockId()') + }) + + it('should only be able to withdraw once full lock period has elapsed', async () => { + await mintLock() + + await expect(sdlPool.withdraw(1, toEther(1))).to.be.revertedWith('UnlockNotInitiated()') + + await time.increase(200 * DAY) + await sdlPool.initiateUnlock(1) + await updateLocks() + + await expect(sdlPool.withdraw(1, toEther(1))).to.be.revertedWith('TotalDurationNotElapsed()') + + await time.increase(200 * DAY) + sdlPool.withdraw(1, toEther(1)) + }) + + it('only the lock owner should be able to withdraw, lock id must be valid', async () => { + await mintLock() + + await time.increase(200 * DAY) + await sdlPool.initiateUnlock(1) + await updateLocks() + await time.increase(200 * DAY) + + await expect(sdlPool.connect(signers[1]).withdraw(1, toEther(1))).to.be.revertedWith( + 'SenderNotAuthorized()' + ) + await expect(sdlPool.withdraw(2, toEther(1))).to.be.revertedWith('InvalidLockId()') + + sdlPool.withdraw(1, toEther(1)) + }) + + it('should be able to transfer ownership of locks using transferFrom', async () => { + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * DAY]) + ) + let ts2 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await sdlToken + .connect(signers[2]) + .transferAndCall( + sdlPool.address, + toEther(300), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 2 * 365 * DAY]) + ) + let ts3 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await updateLocks(1, []) + await sdlPool.connect(signers[1]).executeQueuedOperations([]) + await sdlPool.connect(signers[2]).executeQueuedOperations([]) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(400), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 3 * 365 * DAY]) + ) + let ts4 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + + await updateLocks(4, []) + + await sdlPool.transferFrom(accounts[0], accounts[1], 1) + await sdlPool.connect(signers[2]).transferFrom(accounts[2], accounts[3], 3) + + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 3000) + assert.equal(fromEther(await sdlPool.totalStaked()), 3000) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1, 2, 3, 4])), [ + { amount: 100, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 200, boostAmount: 200, startTime: ts2, duration: 365 * DAY, expiry: 0 }, + { amount: 300, boostAmount: 600, startTime: ts3, duration: 2 * 365 * DAY, expiry: 0 }, + { amount: 400, boostAmount: 1200, startTime: ts4, duration: 3 * 365 * DAY, expiry: 0 }, + ]) + + assert.equal(await sdlPool.ownerOf(1), accounts[1]) + assert.equal(await sdlPool.ownerOf(2), accounts[1]) + assert.equal(await sdlPool.ownerOf(3), accounts[3]) + assert.equal(await sdlPool.ownerOf(4), accounts[0]) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 1600) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 1600) + assert.equal((await sdlPool.balanceOf(accounts[0])).toNumber(), 1) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[0])).map((v) => v.toNumber()), + [4] + ) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[1])), 500) + assert.equal(fromEther(await sdlPool.staked(accounts[1])), 500) + assert.equal((await sdlPool.balanceOf(accounts[1])).toNumber(), 2) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[1])).map((v) => v.toNumber()), + [1, 2] + ) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[2])), 0) + assert.equal(fromEther(await sdlPool.staked(accounts[2])), 0) + assert.equal((await sdlPool.balanceOf(accounts[2])).toNumber(), 0) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[2])).map((v) => v.toNumber()), + [] + ) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[3])), 900) + assert.equal(fromEther(await sdlPool.staked(accounts[3])), 900) + assert.equal((await sdlPool.balanceOf(accounts[3])).toNumber(), 1) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[3])).map((v) => v.toNumber()), + [3] + ) + }) + + it('should be able to transfer ownership of locks using safeTransferFrom', async () => { + const receiver = await deploy('ERC721ReceiverMock') + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * DAY]) + ) + let ts2 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await sdlToken + .connect(signers[2]) + .transferAndCall( + sdlPool.address, + toEther(300), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 2 * 365 * DAY]) + ) + let ts3 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await updateLocks(1, []) + await sdlPool.connect(signers[1]).executeQueuedOperations([]) + await sdlPool.connect(signers[2]).executeQueuedOperations([]) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(400), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 3 * 365 * DAY]) + ) + let ts4 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + + await updateLocks(4, []) + + await sdlPool.functions['safeTransferFrom(address,address,uint256)']( + accounts[0], + accounts[1], + 1 + ) + await sdlPool + .connect(signers[2]) + .functions['safeTransferFrom(address,address,uint256)'](accounts[2], receiver.address, 3) + + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 3000) + assert.equal(fromEther(await sdlPool.totalStaked()), 3000) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1, 2, 3, 4])), [ + { amount: 100, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 200, boostAmount: 200, startTime: ts2, duration: 365 * DAY, expiry: 0 }, + { amount: 300, boostAmount: 600, startTime: ts3, duration: 2 * 365 * DAY, expiry: 0 }, + { amount: 400, boostAmount: 1200, startTime: ts4, duration: 3 * 365 * DAY, expiry: 0 }, + ]) + + assert.equal(await sdlPool.ownerOf(1), accounts[1]) + assert.equal(await sdlPool.ownerOf(2), accounts[1]) + assert.equal(await sdlPool.ownerOf(3), receiver.address) + assert.equal(await sdlPool.ownerOf(4), accounts[0]) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 1600) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 1600) + assert.equal((await sdlPool.balanceOf(accounts[0])).toNumber(), 1) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[0])).map((v) => v.toNumber()), + [4] + ) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[1])), 500) + assert.equal(fromEther(await sdlPool.staked(accounts[1])), 500) + assert.equal((await sdlPool.balanceOf(accounts[1])).toNumber(), 2) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[1])).map((v) => v.toNumber()), + [1, 2] + ) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[2])), 0) + assert.equal(fromEther(await sdlPool.staked(accounts[2])), 0) + assert.equal((await sdlPool.balanceOf(accounts[2])).toNumber(), 0) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[2])).map((v) => v.toNumber()), + [] + ) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(receiver.address)), 900) + assert.equal(fromEther(await sdlPool.staked(receiver.address)), 900) + assert.equal((await sdlPool.balanceOf(receiver.address)).toNumber(), 1) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(receiver.address)).map((v) => v.toNumber()), + [3] + ) + assert.deepEqual( + await receiver.getData().then((d: any) => ({ + operator: d[0].operator, + from: d[0].from, + tokenId: d[0].tokenId.toNumber(), + data: d[0].data, + })), + { + operator: accounts[2], + from: accounts[2], + tokenId: 3, + data: '0x', + } + ) + }) + + it('should be able to transfer ownership of locks using safeTransferFrom with data', async () => { + const receiver = await deploy('ERC721ReceiverMock') + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * DAY]) + ) + let ts2 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await sdlToken + .connect(signers[2]) + .transferAndCall( + sdlPool.address, + toEther(300), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 2 * 365 * DAY]) + ) + let ts3 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await updateLocks(1, []) + await sdlPool.connect(signers[1]).executeQueuedOperations([]) + await sdlPool.connect(signers[2]).executeQueuedOperations([]) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(400), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 3 * 365 * DAY]) + ) + let ts4 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + + await updateLocks(4, []) + + await sdlPool.functions['safeTransferFrom(address,address,uint256,bytes)']( + accounts[0], + accounts[1], + 1, + '0x' + ) + await sdlPool + .connect(signers[2]) + .functions['safeTransferFrom(address,address,uint256,bytes)']( + accounts[2], + receiver.address, + 3, + '0x01' + ) + + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 3000) + assert.equal(fromEther(await sdlPool.totalStaked()), 3000) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1, 2, 3, 4])), [ + { amount: 100, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 200, boostAmount: 200, startTime: ts2, duration: 365 * DAY, expiry: 0 }, + { amount: 300, boostAmount: 600, startTime: ts3, duration: 2 * 365 * DAY, expiry: 0 }, + { amount: 400, boostAmount: 1200, startTime: ts4, duration: 3 * 365 * DAY, expiry: 0 }, + ]) + + assert.equal(await sdlPool.ownerOf(1), accounts[1]) + assert.equal(await sdlPool.ownerOf(2), accounts[1]) + assert.equal(await sdlPool.ownerOf(3), receiver.address) + assert.equal(await sdlPool.ownerOf(4), accounts[0]) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 1600) + assert.equal(fromEther(await sdlPool.staked(accounts[0])), 1600) + assert.equal((await sdlPool.balanceOf(accounts[0])).toNumber(), 1) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[0])).map((v) => v.toNumber()), + [4] + ) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[1])), 500) + assert.equal(fromEther(await sdlPool.staked(accounts[1])), 500) + assert.equal((await sdlPool.balanceOf(accounts[1])).toNumber(), 2) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[1])).map((v) => v.toNumber()), + [1, 2] + ) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[2])), 0) + assert.equal(fromEther(await sdlPool.staked(accounts[2])), 0) + assert.equal((await sdlPool.balanceOf(accounts[2])).toNumber(), 0) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[2])).map((v) => v.toNumber()), + [] + ) + + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(receiver.address)), 900) + assert.equal(fromEther(await sdlPool.staked(receiver.address)), 900) + assert.equal((await sdlPool.balanceOf(receiver.address)).toNumber(), 1) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(receiver.address)).map((v) => v.toNumber()), + [3] + ) + assert.deepEqual( + await receiver.getData().then((d: any) => ({ + operator: d[0].operator, + from: d[0].from, + tokenId: d[0].tokenId.toNumber(), + data: d[0].data, + })), + { + operator: accounts[2], + from: accounts[2], + tokenId: 3, + data: '0x01', + } + ) + }) + + it('safeTransferFrom should revert on transfer to non ERC721 receivers', async () => { + await mintLock() + + await expect( + sdlPool.functions['safeTransferFrom(address,address,uint256,bytes)']( + accounts[0], + sdlToken.address, + 1, + '0x' + ) + ).to.be.revertedWith('TransferToNonERC721Implementer()') + await expect( + sdlPool.functions['safeTransferFrom(address,address,uint256,bytes)']( + accounts[0], + sdlToken.address, + 1, + '0x01' + ) + ).to.be.revertedWith('TransferToNonERC721Implementer()') + }) + + it('token approvals should work correctly', async () => { + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await updateLocks(1, []) + + await expect(sdlPool.connect(signers[2]).approve(accounts[1], 1)).to.be.revertedWith( + 'SenderNotAuthorized()' + ) + await expect(sdlPool.approve(accounts[1], 3)).to.be.revertedWith('InvalidLockId()') + await expect(sdlPool.approve(accounts[0], 1)).to.be.revertedWith('ApprovalToCurrentOwner()') + await expect(sdlPool.getApproved(3)).to.be.revertedWith('InvalidLockId()') + + await sdlPool.approve(accounts[1], 1) + await sdlPool.approve(accounts[2], 2) + assert.equal(await sdlPool.getApproved(1), accounts[1]) + assert.equal(await sdlPool.getApproved(2), accounts[2]) + + await sdlPool.connect(signers[1]).transferFrom(accounts[0], accounts[1], 1) + await sdlPool + .connect(signers[2]) + .functions['safeTransferFrom(address,address,uint256)'](accounts[0], accounts[2], 2) + + assert.equal(await sdlPool.getApproved(1), ethers.constants.AddressZero) + assert.equal(await sdlPool.getApproved(2), ethers.constants.AddressZero) + + await sdlPool.connect(signers[1]).approve(accounts[2], 1) + assert.equal(await sdlPool.getApproved(1), accounts[2]) + await sdlPool.connect(signers[1]).approve(ethers.constants.AddressZero, 1) + assert.equal(await sdlPool.getApproved(1), ethers.constants.AddressZero) + }) + + it('operator approvals should work correctly', async () => { + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await updateLocks(1, []) + + await sdlPool.setApprovalForAll(accounts[1], true) + await sdlPool.setApprovalForAll(accounts[2], true) + + assert.equal(await sdlPool.isApprovedForAll(accounts[0], accounts[1]), true) + assert.equal(await sdlPool.isApprovedForAll(accounts[0], accounts[2]), true) + + await sdlPool.connect(signers[1]).transferFrom(accounts[0], accounts[1], 1) + await sdlPool + .connect(signers[2]) + .functions['safeTransferFrom(address,address,uint256)'](accounts[0], accounts[2], 2) + assert.equal(await sdlPool.isApprovedForAll(accounts[0], accounts[1]), true) + assert.equal(await sdlPool.isApprovedForAll(accounts[0], accounts[2]), true) + + await sdlPool.setApprovalForAll(accounts[1], false) + assert.equal(await sdlPool.isApprovedForAll(accounts[0], accounts[1]), false) + assert.equal(await sdlPool.isApprovedForAll(accounts[0], accounts[2]), true) + }) + + it('should be able to distribute rewards', async () => { + await mintLock(false) + await rewardToken.transferAndCall(sdlPool.address, toEther(1000), '0x') + assert.equal(fromEther(await rewardsPool.withdrawableRewards(accounts[0])), 1000) + }) + + it('creating, modifying, or transferring locks should update rewards', async () => { + await mintLock(false) + await rewardToken.transferAndCall(sdlPool.address, toEther(1000), '0x') + assert.equal(fromEther(await rewardsPool.userRewards(accounts[0])), 0) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await updateLocks(2, []) + assert.equal(fromEther(await rewardsPool.userRewards(accounts[0])), 1000) + + await rewardToken.transferAndCall(sdlPool.address, toEther(1000), '0x') + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 0]) + ) + await updateLocks(0, [1]) + assert.equal(fromEther(await rewardsPool.userRewards(accounts[0])), 2000) + + await rewardToken.transferAndCall(sdlPool.address, toEther(1000), '0x') + await sdlPool.extendLockDuration(2, 10000) + await updateLocks(0, [2]) + assert.equal(fromEther(await rewardsPool.userRewards(accounts[0])), 3000) + + await rewardToken.transferAndCall(sdlPool.address, toEther(1000), '0x') + await time.increase(5000) + await sdlPool.initiateUnlock(2) + assert.equal(fromEther(await rewardsPool.userRewards(accounts[0])), 4000) + + await rewardToken.transferAndCall(sdlPool.address, toEther(1000), '0x') + await time.increase(5000) + await sdlPool.withdraw(2, toEther(10)) + assert.equal(fromEther(await rewardsPool.userRewards(accounts[0])), 5000) + + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(290), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlPool.handleOutgoingUpdate() + await sdlPool.handleIncomingUpdate(3) + await sdlPool.connect(signers[1]).executeQueuedOperations([]) + await rewardToken.transferAndCall(sdlPool.address, toEther(1000), '0x') + await sdlPool.transferFrom(accounts[0], accounts[1], 1) + assert.equal(fromEther(await rewardsPool.userRewards(accounts[0])), 5500) + assert.equal(fromEther(await rewardsPool.userRewards(accounts[1])), 500) + }) + + it('handleOutoingRESDL should work correctly', async () => { + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 20000]) + ) + let ts1 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await updateLocks(1, []) + await time.increase(20000) + await sdlPool.initiateUnlock(2) + let ts2 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await sdlToken.transferAndCall( + sdlPool.address, + toEther(300), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * DAY]) + ) + let ts3 = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + await updateLocks(3, [2]) + + assert.deepEqual( + parseLocks([await sdlPool.callStatic.handleOutgoingRESDL(accounts[0], 1, accounts[4])])[0], + { amount: 100, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 } + ) + await sdlPool.handleOutgoingRESDL(accounts[0], 1, accounts[4]) + await expect(sdlPool.ownerOf(1)).to.be.revertedWith('InvalidLockId()') + assert.equal(fromEther(await sdlToken.balanceOf(accounts[4])), 100) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 800) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 800) + assert.equal((await sdlPool.balanceOf(accounts[0])).toNumber(), 2) + + assert.deepEqual( + parseLocks([await sdlPool.callStatic.handleOutgoingRESDL(accounts[0], 2, accounts[5])])[0], + { amount: 200, boostAmount: 0, startTime: ts1, duration: 20000, expiry: ts2 + 10000 } + ) + await sdlPool.handleOutgoingRESDL(accounts[0], 2, accounts[5]) + await expect(sdlPool.ownerOf(2)).to.be.revertedWith('InvalidLockId()') + assert.equal(fromEther(await sdlToken.balanceOf(accounts[5])), 200) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 600) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 600) + assert.equal((await sdlPool.balanceOf(accounts[0])).toNumber(), 1) + + assert.deepEqual( + parseLocks([await sdlPool.callStatic.handleOutgoingRESDL(accounts[0], 3, accounts[6])])[0], + { amount: 300, boostAmount: 300, startTime: ts3, duration: 365 * DAY, expiry: 0 } + ) + await sdlPool.handleOutgoingRESDL(accounts[0], 3, accounts[6]) + await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') + assert.equal(fromEther(await sdlToken.balanceOf(accounts[6])), 300) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 0) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 0) + assert.equal((await sdlPool.balanceOf(accounts[0])).toNumber(), 0) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await updateLocks(4, []) + await sdlPool.connect(signers[1]).executeQueuedOperations([]) + await rewardToken.transferAndCall(sdlPool.address, toEther(10000), '0x') + + let rewards1 = await rewardsPool.withdrawableRewards(accounts[1]) + let rewards2 = await rewardsPool.withdrawableRewards(accounts[0]) + + await sdlPool.handleOutgoingRESDL(accounts[0], 4, accounts[2]) + + assert.isTrue((await rewardsPool.withdrawableRewards(accounts[1])).eq(rewards1)) + assert.isTrue((await rewardsPool.withdrawableRewards(accounts[0])).eq(rewards2)) + }) + + it('handleIncomingRESDL should work correctly', async () => { + await mintLock(false) + await expect( + sdlPool.handleIncomingRESDL(accounts[1], 1, toEther(100), toEther(50), 123, 456, 789) + ).to.be.revertedWith('InvalidLockId()') + + await sdlPool.handleIncomingRESDL(accounts[1], 7, toEther(100), toEther(50), 123, 456, 0) + assert.deepEqual(parseLocks(await sdlPool.getLocks([7]))[0], { + amount: 100, + boostAmount: 50, + startTime: 123, + duration: 456, + expiry: 0, + }) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 250) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[1])), 150) + assert.equal(await sdlPool.ownerOf(7), accounts[1]) + assert.equal((await sdlPool.balanceOf(accounts[1])).toNumber(), 1) + assert.equal((await sdlPool.lastLockId()).toNumber(), 7) + + await sdlPool.handleIncomingRESDL(accounts[2], 9, toEther(200), toEther(400), 1, 2, 3) + assert.deepEqual(parseLocks(await sdlPool.getLocks([9]))[0], { + amount: 200, + boostAmount: 400, + startTime: 1, + duration: 2, + expiry: 3, + }) + assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 850) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[2])), 600) + assert.equal(await sdlPool.ownerOf(9), accounts[2]) + assert.equal((await sdlPool.balanceOf(accounts[2])).toNumber(), 1) + assert.equal((await sdlPool.lastLockId()).toNumber(), 9) + + await rewardToken.transferAndCall(sdlPool.address, toEther(10000), '0x') + + let rewards1 = await rewardsPool.withdrawableRewards(accounts[3]) + let rewards2 = await rewardsPool.withdrawableRewards(accounts[0]) + + await sdlPool.handleIncomingRESDL(accounts[3], 10, toEther(50), toEther(100), 1, 2, 3) + + assert.isTrue((await rewardsPool.withdrawableRewards(accounts[3])).eq(rewards1)) + assert.isTrue((await rewardsPool.withdrawableRewards(accounts[0])).eq(rewards2)) + }) + + it('handleOutgoingUpdate and handleIncomingUpdate should work correctly', async () => { + assert.equal(await sdlPool.shouldUpdate(), false) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + + assert.equal(await sdlPool.shouldUpdate(), true) + assert.equal((await sdlPool.updateBatchIndex()).toNumber(), 1) + assert.deepEqual( + await sdlPool.callStatic + .handleOutgoingUpdate() + .then((d) => [d[0].toNumber(), fromEther(d[1])]), + [2, 300] + ) + await sdlPool.handleOutgoingUpdate() + assert.equal((await sdlPool.updateInProgress()).toNumber(), 1) + assert.equal(await sdlPool.shouldUpdate(), false) + assert.equal((await sdlPool.updateBatchIndex()).toNumber(), 2) + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0) + await expect(sdlPool.handleOutgoingUpdate()).to.be.revertedWith('UpdateInProgress()') + + await sdlPool.handleIncomingUpdate(7) + assert.equal((await sdlPool.lastLockId()).toNumber(), 8) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[0])), [ + [ + { + amount: 100, + boostAmount: 0, + startTime: 0, + duration: 0, + expiry: 0, + }, + ], + [1], + ]) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[1])), [ + [ + { + amount: 200, + boostAmount: 0, + startTime: 0, + duration: 0, + expiry: 0, + }, + ], + [1], + ]) + await expect(sdlPool.handleIncomingUpdate(0)).to.be.revertedWith('NoUpdateInProgress()') + + await sdlPool.executeQueuedOperations([]) + await sdlPool.connect(signers[1]).executeQueuedOperations([]) + + assert.equal(await sdlPool.ownerOf(7), accounts[0]) + assert.equal(await sdlPool.ownerOf(8), accounts[1]) + + await sdlPool.withdraw(7, toEther(60)) + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(10), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [8, 0]) + ) + + assert.equal(await sdlPool.shouldUpdate(), true) + assert.equal((await sdlPool.updateBatchIndex()).toNumber(), 2) + assert.deepEqual( + await sdlPool.callStatic + .handleOutgoingUpdate() + .then((d) => [d[0].toNumber(), fromEther(d[1])]), + [0, -50] + ) + await sdlPool.handleOutgoingUpdate() + assert.equal((await sdlPool.updateInProgress()).toNumber(), 1) + assert.equal(await sdlPool.shouldUpdate(), false) + assert.equal((await sdlPool.updateBatchIndex()).toNumber(), 3) + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0) + await expect(sdlPool.handleOutgoingUpdate()).to.be.revertedWith('UpdateInProgress()') + + await sdlPool.handleIncomingUpdate(0) + assert.equal((await sdlPool.lastLockId()).toNumber(), 8) + assert.deepEqual(parseLockUpdates(await sdlPool.getQueuedLockUpdates([7, 8])), [ + [ + { + updateBatchIndex: 2, + lock: { amount: 40, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + }, + ], + [ + { + updateBatchIndex: 2, + lock: { amount: 210, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + }, + ], + ]) + + await expect(sdlPool.handleIncomingUpdate(0)).to.be.revertedWith('NoUpdateInProgress()') + }) + + it('queueing new locks should work correctly', async () => { + assert.equal(await sdlPool.shouldUpdate(), false) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + + assert.equal(await sdlPool.shouldUpdate(), true) + + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 300) + await updateLocks(1, []) + await sdlPool.executeQueuedOperations([]) + + assert.equal(await sdlPool.shouldUpdate(), false) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[0])), [[], []]) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[1])), [ + [ + { + amount: 200, + boostAmount: 0, + startTime: 0, + duration: 0, + expiry: 0, + }, + ], + [1], + ]) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(300), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + + assert.equal(await sdlPool.shouldUpdate(), true) + + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(400), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + + assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 700) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[0])), [ + [ + { + amount: 300, + boostAmount: 0, + startTime: 0, + duration: 0, + expiry: 0, + }, + ], + [2], + ]) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[1])), [ + [ + { + amount: 200, + boostAmount: 0, + startTime: 0, + duration: 0, + expiry: 0, + }, + { + amount: 400, + boostAmount: 0, + startTime: 0, + duration: 0, + expiry: 0, + }, + ], + [1, 2], + ]) + + await updateLocks(3, []) + await updateLocks(0, []) + + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(500), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[0])), [[], []]) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[1])), [ + [ + { + amount: 200, + boostAmount: 0, + startTime: 0, + duration: 0, + expiry: 0, + }, + { + amount: 400, + boostAmount: 0, + startTime: 0, + duration: 0, + expiry: 0, + }, + { + amount: 500, + boostAmount: 0, + startTime: 0, + duration: 0, + expiry: 0, + }, + ], + [1, 2, 4], + ]) + + await updateLocks(12, []) + await sdlPool.connect(signers[1]).executeQueuedOperations([]) + + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[0])).map((v) => v.toNumber()), + [1, 3] + ) + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[1])).map((v) => v.toNumber()), + [2, 4, 12] + ) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1, 2, 3, 4, 12])), [ + { amount: 100, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 200, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 300, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 400, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 500, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + ]) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(600), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlPool.handleOutgoingUpdate() + await sdlToken.transferAndCall( + sdlPool.address, + toEther(700), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlPool.handleIncomingUpdate(15) + await sdlPool.executeQueuedOperations([]) + + assert.deepEqual( + (await sdlPool.getLockIdsByOwner(accounts[0])).map((v) => v.toNumber()), + [1, 3, 15] + ) + assert.deepEqual(parseNewLocks(await sdlPool.getQueuedNewLocksByOwner(accounts[0])), [ + [ + { + amount: 700, + boostAmount: 0, + startTime: 0, + duration: 0, + expiry: 0, + }, + ], + [6], + ]) + }) + + it('queueing lock updates should work correctly', async () => { + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await updateLocks(1, []) + await sdlPool.connect(signers[1]).executeQueuedOperations([]) + + assert.equal(await sdlPool.shouldUpdate(), false) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 0]) + ) + + assert.equal(await sdlPool.shouldUpdate(), true) + + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [2, 0]) + ) + + assert.deepEqual(parseLockUpdates(await sdlPool.getQueuedLockUpdates([1, 2])), [ + [ + { + updateBatchIndex: 2, + lock: { amount: 200, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + }, + ], + [ + { + updateBatchIndex: 2, + lock: { amount: 400, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + }, + ], + ]) + + await updateLocks(0, [1]) + + assert.deepEqual(parseLockUpdates(await sdlPool.getQueuedLockUpdates([1, 2])), [ + [], + [ + { + updateBatchIndex: 2, + lock: { amount: 400, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + }, + ], + ]) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1, 2])), [ + { amount: 200, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 200, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + ]) + + await updateLocks(0, []) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 0]) + ) + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [2, 0]) + ) + await updateLocks(0, []) + + assert.deepEqual(parseLockUpdates(await sdlPool.getQueuedLockUpdates([1, 2])), [ + [ + { + updateBatchIndex: 4, + lock: { amount: 300, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + }, + ], + [ + { + updateBatchIndex: 2, + lock: { amount: 400, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + }, + { + updateBatchIndex: 4, + lock: { amount: 600, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + }, + ], + ]) + + await sdlPool.executeQueuedOperations([1]) + await sdlPool.connect(signers[1]).executeQueuedOperations([2]) + await sdlPool.executeQueuedOperations([1]) + await sdlPool.connect(signers[1]).executeQueuedOperations([2]) + + assert.deepEqual(parseLockUpdates(await sdlPool.getQueuedLockUpdates([1, 2])), [[], []]) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1, 2])), [ + { amount: 300, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 600, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + ]) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 0]) + ) + await sdlPool.handleOutgoingUpdate() + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 0]) + ) + await sdlPool.handleIncomingUpdate(0) + await sdlPool.executeQueuedOperations([1]) + + assert.deepEqual(parseLockUpdates(await sdlPool.getQueuedLockUpdates([1, 2])), [ + [ + { + updateBatchIndex: 6, + lock: { amount: 500, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + }, + ], + [], + ]) + assert.deepEqual(parseLocks(await sdlPool.getLocks([1, 2])), [ + { amount: 400, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + { amount: 600, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, + ]) + }) +}) From 23bde838329814d67f79c23f0379118095682c05 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Fri, 10 Nov 2023 14:02:38 -0500 Subject: [PATCH 13/42] fixed failing tests --- test/core/ccip/resdl-token-bridge.test.ts | 9 ++++----- test/core/slashing-keeper.test.ts | 6 ++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/test/core/ccip/resdl-token-bridge.test.ts b/test/core/ccip/resdl-token-bridge.test.ts index c5333667..812c7cf9 100644 --- a/test/core/ccip/resdl-token-bridge.test.ts +++ b/test/core/ccip/resdl-token-bridge.test.ts @@ -8,8 +8,8 @@ import { CCIPTokenPoolMock, WrappedNative, RESDLTokenBridge, - SDLPool, SDLPoolCCIPControllerMock, + SDLPoolPrimary, } from '../../../typechain-types' import { time } from '@nomicfoundation/hardhat-network-helpers' import { Signer } from 'ethers' @@ -19,7 +19,7 @@ describe('RESDLTokenBridge', () => { let sdlToken: ERC677 let token2: ERC677 let bridge: RESDLTokenBridge - let sdlPool: SDLPool + let sdlPool: SDLPoolPrimary let onRamp: CCIPOnRampMock let offRamp: CCIPOffRampMock let tokenPool: CCIPTokenPoolMock @@ -56,13 +56,12 @@ describe('RESDLTokenBridge', () => { await router.applyRampUpdates([[77, onRamp.address]], [], [[77, offRamp.address]]) let boostController = await deploy('LinearBoostController', [4 * 365 * 86400, 4]) - sdlPool = (await deployUpgradeable('SDLPool', [ + sdlPool = (await deployUpgradeable('SDLPoolPrimary', [ 'reSDL', 'reSDL', sdlToken.address, boostController.address, - ethers.constants.AddressZero, - ])) as SDLPool + ])) as SDLPoolPrimary let sdlPoolCCIPController = (await deploy('SDLPoolCCIPControllerMock', [ sdlToken.address, sdlPool.address, diff --git a/test/core/slashing-keeper.test.ts b/test/core/slashing-keeper.test.ts index eaaca828..754229ba 100644 --- a/test/core/slashing-keeper.test.ts +++ b/test/core/slashing-keeper.test.ts @@ -94,8 +94,7 @@ describe('SlashingKeeper', () => { assert.equal(data[0], true, 'upkeepNeeded incorrect') assert.deepEqual( decode(data[1])[0].map((v: any) => v.toNumber()), - [2], - 'performData incorrect' + [2] ) await strategy1.simulateSlash(toEther(30)) @@ -104,8 +103,7 @@ describe('SlashingKeeper', () => { assert.equal(data[0], true, 'upkeepNeeded incorrect') assert.deepEqual( decode(data[1])[0].map((v: any) => v.toNumber()), - [0, 2], - 'performData incorrect' + [0, 2] ) }) From 25a02f200cfa1899ecca66314807e0c3f1c054e7 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Wed, 15 Nov 2023 11:10:44 -0500 Subject: [PATCH 14/42] disallow adding SDL rewards to SDL pool --- contracts/core/base/RewardsPoolController.sol | 2 +- contracts/core/sdlPool/base/SDLPool.sol | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/contracts/core/base/RewardsPoolController.sol b/contracts/core/base/RewardsPoolController.sol index 25ab6d53..024d002b 100644 --- a/contracts/core/base/RewardsPoolController.sol +++ b/contracts/core/base/RewardsPoolController.sol @@ -148,7 +148,7 @@ abstract contract RewardsPoolController is UUPSUpgradeable, OwnableUpgradeable { * @param _token token to add * @param _rewardsPool token rewards pool to add **/ - function addToken(address _token, address _rewardsPool) public onlyOwner { + function addToken(address _token, address _rewardsPool) public virtual onlyOwner { require(!isTokenSupported(_token), "Token is already supported"); tokenPools[_token] = IRewardsPool(_rewardsPool); diff --git a/contracts/core/sdlPool/base/SDLPool.sol b/contracts/core/sdlPool/base/SDLPool.sol index 679fe280..4912667c 100644 --- a/contracts/core/sdlPool/base/SDLPool.sol +++ b/contracts/core/sdlPool/base/SDLPool.sol @@ -82,6 +82,7 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp error DuplicateContract(); error ContractNotFound(); error UnlockAlreadyInitiated(); + error InvalidToken(); /** * @notice initializes contract @@ -328,6 +329,16 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp return totalEffectiveBalance; } + /** + * @notice adds a new token + * @param _token token to add + * @param _rewardsPool token rewards pool to add + **/ + function addToken(address _token, address _rewardsPool) public override onlyOwner { + if (_token == address(sdlToken)) revert InvalidToken(); + super.addToken(_token, _rewardsPool); + } + /** * @notice returns whether this contract supports an interface * @param _interfaceId id of interface From 2dc6e7af6d26d5040b7b3242e999c5b96d965594 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Thu, 16 Nov 2023 18:05:25 -0500 Subject: [PATCH 15/42] sdl pool ccip controller tests --- .../ccip/SDLPoolCCIPControllerPrimary.sol | 93 ++++- .../ccip/SDLPoolCCIPControllerSecondary.sol | 85 +++- .../core/ccip/base/SDLPoolCCIPController.sol | 19 +- .../interfaces/IRewardsPoolController.sol | 2 +- .../core/test/chainlink/CCIPOffRampMock.sol | 4 + .../core/test/chainlink/CCIPOnRampMock.sol | 20 +- test/core/ccip/resdl-token-bridge.test.ts | 6 +- .../sdl-pool-ccip-controller-primary.test.ts | 374 ++++++++++++++++++ ...sdl-pool-ccip-controller-secondary.test.ts | 332 ++++++++++++++++ test/core/ccip/wrapped-token-bridge.test.ts | 6 +- 10 files changed, 905 insertions(+), 36 deletions(-) create mode 100644 test/core/ccip/sdl-pool-ccip-controller-primary.test.ts create mode 100644 test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts diff --git a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol index afb78aa9..ecdb594e 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol @@ -14,8 +14,8 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { uint64[] internal whitelistedChains; mapping(uint64 => address) public whitelistedDestinations; - mapping(uint64 => bytes) public extraArgsByChain; + mapping(uint64 => bytes) public extraArgsByChain; mapping(uint64 => uint256) public reSDLSupplyByChain; mapping(address => address) public wrappedRewardTokens; @@ -24,21 +24,28 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { event ChainAdded(uint64 indexed chainSelector, address destination, bytes extraArgs); event ChainRemoved(uint64 indexed destinationChainSelector, address destination); event SetExtraArgs(uint64 indexed chainSelector, bytes extraArgs); + event SetWrappedRewardToken(address indexed token, address rewardToken); /** - * @notice Initializes the contractMessageSent + * @notice Initializes the contract * @param _router address of the CCIP router * @param _linkToken address of the LINK token * @param _sdlToken address of the SDL token * @param _sdlPool address of the SDL Pool + * @param _maxLINKFee max fee to be paid on an outgoing message **/ constructor( address _router, address _linkToken, address _sdlToken, - address _sdlPool - ) SDLPoolCCIPController(_router, _linkToken, _sdlToken, _sdlPool) {} + address _sdlPool, + uint256 _maxLINKFee + ) SDLPoolCCIPController(_router, _linkToken, _sdlToken, _sdlPool, _maxLINKFee) {} + /** + * @notice Claims and distributes rewards between all secondary chains + * @param _extraArgs list of extra args as defined in CCIP API to be used for distribution to each chain + **/ function distributeRewards(bytes[] memory _extraArgs) external { uint256 totalRESDL = ISDLPoolPrimary(sdlPool).effectiveBalanceOf(address(this)); address[] memory tokens = ISDLPoolPrimary(sdlPool).supportedTokens(); @@ -58,7 +65,7 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { address wrappedToken = wrappedRewardTokens[token]; if (wrappedToken != address(0)) { IERC677(token).transferAndCall(wrappedToken, tokenBalance, ""); - token = wrappedToken; + tokens[i] = wrappedToken; tokenBalance = IERC20(wrappedToken).balanceOf(address(this)); } @@ -78,6 +85,13 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { } } + /** + * @notice Handles the outgoing transfer of an reSDL token to another chain + * @param _destinationChainSelector id of the destination chain + * @param _sender sender of the transfer + * @param _tokenId id of token + * @return the token being transferred + **/ function handleOutgoingRESDL( uint64 _destinationChainSelector, address _sender, @@ -99,6 +113,17 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { return (amount, boostAmount, startTime, duration, expiry); } + /** + * @notice Handles the incoming transfer of an reSDL token from another chain + * @param _sourceChainSelector id of the source chain + * @param _receiver receiver of the transfer + * @param _tokenId id of reSDL token + * @param _amount amount of underlying SDL + * @param _boostAmount reSDL boost amount + * @param _startTime start time of the lock + * @param _duration duration of the lock + * @param _expiry expiry time of the lock + **/ function handleIncomingRESDL( uint64 _sourceChainSelector, address _receiver, @@ -122,6 +147,10 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { reSDLSupplyByChain[_sourceChainSelector] -= _amount + _boostAmount; } + /** + * @notice Returns a list of all whitelisted chains + * @return list of whitelisted chain ids + **/ function getWhitelistedChains() external view returns (uint64[] memory) { return whitelistedChains; } @@ -164,12 +193,45 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { delete extraArgsByChain[_chainSelector]; } + /** + * @notice Approves the CCIP router to transfer tokens on behalf of this contract + * @param _tokens list of tokens to approve + **/ + function approveRewardTokens(address[] calldata _tokens) external onlyOwner { + address router = getRouter(); + for (uint256 i = 0; i < _tokens.length; i++) { + IERC20(_tokens[i]).safeApprove(router, type(uint256).max); + } + } + + /** + * @notice Sets the wrapped token address for a reward token + * @param _token address of token + * @param _wrappedToken address of wrapped token + **/ + function setWrappedRewardToken(address _token, address _wrappedToken) external onlyOwner { + wrappedRewardTokens[_token] = _wrappedToken; + emit SetWrappedRewardToken(_token, _wrappedToken); + } + + /** + * @notice Sets the extra args for a chain + * @param _chainSelector id of chain + * @param _extraArgs extra args as defined in CCIP API + **/ function setExtraArgs(uint64 _chainSelector, bytes calldata _extraArgs) external onlyOwner { if (whitelistedDestinations[_chainSelector] == address(0)) revert InvalidDestination(); extraArgsByChain[_chainSelector] = _extraArgs; emit SetExtraArgs(_chainSelector, _extraArgs); } + /** + * @notice Distributes rewards to a single chain + * @param _destinationChainSelector id of chain + * @param _extraArgs extra args as defined in CCIP API + * @param _rewardTokens list of reward tokens to distribute + * @param _rewardTokenAmounts list of reward token amounts to distribute + **/ function _distributeRewards( uint64 _destinationChainSelector, bytes memory _extraArgs, @@ -216,6 +278,11 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { emit DistributeRewards(messageId, _destinationChainSelector, fees); } + /** + * @notice Processes a received message + * @dev handles incoming updates from a secondary chain and sends an update in response + * @param _any2EvmMessage CCIP message + **/ function _ccipReceive(Client.Any2EVMMessage memory _any2EvmMessage) internal override { address sender = abi.decode(_any2EvmMessage.sender, (address)); uint64 sourceChainSelector = _any2EvmMessage.sourceChainSelector; @@ -225,7 +292,7 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { if (totalRESDLSupplyChange > 0) { reSDLSupplyByChain[sourceChainSelector] += uint256(totalRESDLSupplyChange); - } else if (totalRESDLSupplyChange > 0) { + } else if (totalRESDLSupplyChange < 0) { reSDLSupplyByChain[sourceChainSelector] -= uint256(-1 * totalRESDLSupplyChange); } @@ -236,6 +303,11 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { emit MessageReceived(_any2EvmMessage.messageId, sourceChainSelector); } + /** + * @notice Sends an update to a secondary chain + * @param _destinationChainSelector id of destination chain + * @param _mintStartIndex first index to be used for minting new reSDL tokens + **/ function _ccipSendUpdate(uint64 _destinationChainSelector, uint256 _mintStartIndex) internal { Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( whitelistedDestinations[_destinationChainSelector], @@ -254,6 +326,15 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { emit MessageSent(messageId, _destinationChainSelector, fees); } + /** + * @notice Builds a CCIP message + * @dev builds the message for reward distribution or outgoing updates to a secondary chain + * @param _destination address of destination contract + * @param _mintStartIndex first index to be used for minting new reSDL tokens + * @param _tokens list of tokens to transfer + * @param _tokenAmounts list of token amounts to transfer + * @param _extraArgs encoded args as defined in CCIP API + **/ function _buildCCIPMessage( address _destination, uint256 _mintStartIndex, diff --git a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol index 8cfaa04a..2e3e0d54 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol @@ -12,20 +12,30 @@ interface ISDLPoolSecondary is ISDLPool { function shouldUpdate() external view returns (bool); } -contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { +contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { using SafeERC20 for IERC20; - uint64 internal timeOfLastUpdate; - uint64 internal timeBetweenUpdates; + uint64 public timeOfLastUpdate; + uint64 public timeBetweenUpdates; - uint64 internal primaryChainSelector; - address internal primaryChainDestination; - bytes internal extraArgs; - - event SetPrimaryChain(uint64 primaryChainSelector, address primaryChainDestination); + uint64 public immutable primaryChainSelector; + address public immutable primaryChainDestination; + bytes public extraArgs; error UpdateConditionsNotMet(); + /** + * @notice Initializes the contract + * @param _router address of the CCIP router + * @param _linkToken address of the LINK token + * @param _sdlToken address of the SDL token + * @param _sdlPool address of the SDL Pool + * @param _primaryChainSelector id of the primary chain + * @param _primaryChainDestination address to receive messages on primary chain + * @param _maxLINKFee max fee to be paid on an outgoing message + * @param _timeBetweenUpdates min amount of time (seconds) between updates + * @param _extraArgs extra args as defined in CCIP API to be used for outgoing messages + **/ constructor( address _router, address _linkToken, @@ -33,13 +43,21 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { address _sdlPool, uint64 _primaryChainSelector, address _primaryChainDestination, + uint256 _maxLINKFee, + uint64 _timeBetweenUpdates, bytes memory _extraArgs - ) SDLPoolCCIPController(_router, _linkToken, _sdlToken, _sdlPool) { + ) SDLPoolCCIPController(_router, _linkToken, _sdlToken, _sdlPool, _maxLINKFee) { primaryChainSelector = _primaryChainSelector; primaryChainDestination = _primaryChainDestination; + timeBetweenUpdates = _timeBetweenUpdates; extraArgs = _extraArgs; } + /** + * @notice Returns whether an update to the primary chain should be initiated + * @dev used by Chainlink automation + * @return whether an update should be initiated + **/ function checkUpkeep(bytes calldata) external view returns (bool, bytes memory) { if (ISDLPoolSecondary(sdlPool).shouldUpdate() && block.timestamp > timeOfLastUpdate + timeBetweenUpdates) { return (true, "0x"); @@ -48,6 +66,10 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { return (false, "0x"); } + /** + * @notice Initiates an update to the primary chain if update conditions are met + * @dev used by Chainlink automation + **/ function performUpkeep(bytes calldata) external { if (!ISDLPoolSecondary(sdlPool).shouldUpdate() || block.timestamp <= timeOfLastUpdate + timeBetweenUpdates) revert UpdateConditionsNotMet(); @@ -56,6 +78,12 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { _initiateUpdate(primaryChainSelector, primaryChainDestination, extraArgs); } + /** + * @notice Handles the outgoing transfer of an reSDL token to the primary chain + * @param _sender sender of the transfer + * @param _tokenId id of token + * @return the token being transferred + **/ function handleOutgoingRESDL(address _sender, uint256 _tokenId) external onlyBridge @@ -70,6 +98,16 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { return ISDLPoolSecondary(sdlPool).handleOutgoingRESDL(_sender, _tokenId, reSDLTokenBridge); } + /** + * @notice Handles the incoming transfer of an reSDL token from the primary chain + * @param _receiver receiver of the transfer + * @param _tokenId id of reSDL token + * @param _amount amount of underlying SDL + * @param _boostAmount reSDL boost amount + * @param _startTime start time of the lock + * @param _duration duration of the lock + * @param _expiry expiry time of the lock + **/ function handleIncomingRESDL( address _receiver, uint256 _tokenId, @@ -91,12 +129,20 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { ); } - function setPrimaryChain(uint64 _primaryChainSelector, address _primaryChainDestination) external onlyOwner { - primaryChainSelector = _primaryChainSelector; - primaryChainDestination = _primaryChainDestination; - emit SetPrimaryChain(_primaryChainSelector, _primaryChainDestination); + /** + * @notice Sets the min amount of time between updates + * @param _timeBetweenUpdates min amount of time (seconds) + **/ + function setTimeBetweenUpdates(uint64 _timeBetweenUpdates) external onlyOwner { + timeBetweenUpdates = _timeBetweenUpdates; } + /** + * @notice Initiates an update to the primary chain + * @param _destinationChainSelector id of destination chain + * @param _destination address to receive message on destination chain + * @param _extraArgs extra args as defined in CCIP API + **/ function _initiateUpdate( uint64 _destinationChainSelector, address _destination, @@ -120,6 +166,11 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { emit MessageSent(messageId, _destinationChainSelector, fees); } + /** + * @notice Processes a received message + * @dev handles incoming updates and reward distributions from the primary chain + * @param _any2EvmMessage CCIP message + **/ function _ccipReceive(Client.Any2EVMMessage memory _any2EvmMessage) internal override { address sender = abi.decode(_any2EvmMessage.sender, (address)); uint64 sourceChainSelector = _any2EvmMessage.sourceChainSelector; @@ -143,6 +194,14 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { emit MessageReceived(_any2EvmMessage.messageId, sourceChainSelector); } + /** + * @notice Builds a CCIP message + * @dev builds the message for outgoing updates to the primary chain + * @param _destination address of destination contract + * @param _numNewRESDLTokens number of new reSDL NFTs to be minted + * @param _totalRESDLSupplyChange reSDL supply change since last update + * @param _extraArgs encoded args as defined in CCIP API + **/ function _buildCCIPMessage( address _destination, uint256 _numNewRESDLTokens, diff --git a/contracts/core/ccip/base/SDLPoolCCIPController.sol b/contracts/core/ccip/base/SDLPoolCCIPController.sol index bb554db6..b984cdc9 100644 --- a/contracts/core/ccip/base/SDLPoolCCIPController.sol +++ b/contracts/core/ccip/base/SDLPoolCCIPController.sol @@ -10,10 +10,10 @@ import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { using SafeERC20 for IERC20; - IERC20 linkToken; + IERC20 public immutable linkToken; - IERC20 public sdlToken; - address public sdlPool; + IERC20 public immutable sdlToken; + address public immutable sdlPool; address public reSDLTokenBridge; uint256 public maxLINKFee; @@ -45,16 +45,19 @@ abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { * @param _linkToken address of the LINK token * @param _sdlToken address of the SDL token * @param _sdlPool address of the SDL Pool + * @param _maxLINKFee max fee to be paid on an outgoing message **/ constructor( address _router, address _linkToken, address _sdlToken, - address _sdlPool + address _sdlPool, + uint256 _maxLINKFee ) CCIPReceiver(_router) { linkToken = IERC20(_linkToken); sdlToken = IERC20(_sdlToken); sdlPool = _sdlPool; + maxLINKFee = _maxLINKFee; linkToken.approve(_router, type(uint256).max); } @@ -70,10 +73,18 @@ abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { } } + /** + * @notice Sets the max LINK fee to be paid on an outgoing CCIP message + * @param _maxLINKFee maximum fee in LINK + **/ function setMaxLINKFee(uint256 _maxLINKFee) external onlyOwner { maxLINKFee = _maxLINKFee; } + /** + * @notice Sets the address of the reSDL token bridge + * @param _reSDLTokenBridge address of reSDL token bridge + **/ function setRESDLTokenBridge(address _reSDLTokenBridge) external onlyOwner { reSDLTokenBridge = _reSDLTokenBridge; } diff --git a/contracts/core/interfaces/IRewardsPoolController.sol b/contracts/core/interfaces/IRewardsPoolController.sol index 331b7e69..ee779481 100644 --- a/contracts/core/interfaces/IRewardsPoolController.sol +++ b/contracts/core/interfaces/IRewardsPoolController.sol @@ -25,5 +25,5 @@ interface IRewardsPoolController { function distributeTokens(address[] memory _tokens) external; - function withdrawRewards(address[] memory _tokens) external view; + function withdrawRewards(address[] memory _tokens) external; } diff --git a/contracts/core/test/chainlink/CCIPOffRampMock.sol b/contracts/core/test/chainlink/CCIPOffRampMock.sol index f08d90c6..33c654cf 100644 --- a/contracts/core/test/chainlink/CCIPOffRampMock.sol +++ b/contracts/core/test/chainlink/CCIPOffRampMock.sol @@ -49,4 +49,8 @@ contract CCIPOffRampMock { _receiver ); } + + function setTokenPool(address _token, address _pool) external { + tokenPools[_token] = ITokenPool(_pool); + } } diff --git a/contracts/core/test/chainlink/CCIPOnRampMock.sol b/contracts/core/test/chainlink/CCIPOnRampMock.sol index 7ba8668e..11be30ed 100644 --- a/contracts/core/test/chainlink/CCIPOnRampMock.sol +++ b/contracts/core/test/chainlink/CCIPOnRampMock.sol @@ -8,7 +8,7 @@ import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.s * @notice Mocks CCIP onramp contract for testing */ contract CCIPOnRampMock { - struct LastRequestData { + struct RequestData { uint256 feeTokenAmount; address originalSender; } @@ -16,8 +16,8 @@ contract CCIPOnRampMock { mapping(address => address) public tokenPools; address public linkToken; - Client.EVM2AnyMessage private lastRequestMessage; - LastRequestData public lastRequestData; + Client.EVM2AnyMessage[] public requestMessages; + RequestData[] public requestData; constructor( address[] memory _tokens, @@ -31,7 +31,11 @@ contract CCIPOnRampMock { } function getLastRequestMessage() external view returns (Client.EVM2AnyMessage memory) { - return lastRequestMessage; + return requestMessages[requestMessages.length - 1]; + } + + function getLastRequestData() external view returns (RequestData memory) { + return requestData[requestData.length - 1]; } function getFee(Client.EVM2AnyMessage calldata _message) external view returns (uint256) { @@ -47,8 +51,12 @@ contract CCIPOnRampMock { uint256 _feeTokenAmount, address _originalSender ) external returns (bytes32) { - lastRequestMessage = _message; - lastRequestData = LastRequestData(_feeTokenAmount, _originalSender); + requestMessages.push(_message); + requestData.push(RequestData(_feeTokenAmount, _originalSender)); return keccak256(abi.encode(block.timestamp)); } + + function setTokenPool(address _token, address _pool) external { + tokenPools[_token] = _pool; + } } diff --git a/test/core/ccip/resdl-token-bridge.test.ts b/test/core/ccip/resdl-token-bridge.test.ts index 812c7cf9..d19a1934 100644 --- a/test/core/ccip/resdl-token-bridge.test.ts +++ b/test/core/ccip/resdl-token-bridge.test.ts @@ -109,7 +109,7 @@ describe('RESDLTokenBridge', () => { let preFeeBalance = await linkToken.balanceOf(accounts[0]) await bridge.transferRESDL(77, accounts[4], 2, false, toEther(10), '0x') - let lastRequestData = await onRamp.lastRequestData() + let lastRequestData = await onRamp.getLastRequestData() let lastRequestMsg = await onRamp.getLastRequestMessage() assert.equal(fromEther(await sdlToken.balanceOf(tokenPool.address)), 1000) @@ -156,7 +156,7 @@ describe('RESDLTokenBridge', () => { preFeeBalance = await linkToken.balanceOf(accounts[0]) await bridge.transferRESDL(77, accounts[5], 3, false, toEther(10), '0x') - lastRequestData = await onRamp.lastRequestData() + lastRequestData = await onRamp.getLastRequestData() lastRequestMsg = await onRamp.getLastRequestMessage() assert.equal(fromEther(await sdlToken.balanceOf(tokenPool.address)), 1500) @@ -196,7 +196,7 @@ describe('RESDLTokenBridge', () => { let preFeeBalance = await ethers.provider.getBalance(accounts[0]) await bridge.transferRESDL(77, accounts[4], 2, true, toEther(10), '0x', { value: toEther(10) }) - let lastRequestData = await onRamp.lastRequestData() + let lastRequestData = await onRamp.getLastRequestData() let lastRequestMsg = await onRamp.getLastRequestMessage() assert.equal(fromEther(await sdlToken.balanceOf(tokenPool.address)), 1000) diff --git a/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts b/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts new file mode 100644 index 00000000..5fbf8b99 --- /dev/null +++ b/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts @@ -0,0 +1,374 @@ +import { ethers } from 'hardhat' +import { assert, expect } from 'chai' +import { toEther, deploy, deployUpgradeable, getAccounts, fromEther } from '../../utils/helpers' +import { + ERC677, + CCIPOnRampMock, + CCIPOffRampMock, + CCIPTokenPoolMock, + SDLPoolPrimary, + SDLPoolCCIPControllerPrimary, + Router, +} from '../../../typechain-types' +import { Signer } from 'ethers' + +const parseLock = (lock: any) => ({ + amount: fromEther(lock[0]), + boostAmount: Number(fromEther(lock[1]).toFixed(4)), + startTime: lock[2].toNumber(), + duration: lock[3].toNumber(), + expiry: lock[4].toNumber(), +}) + +describe('SDLPoolCCIPControllerPrimary', () => { + let linkToken: ERC677 + let sdlToken: ERC677 + let token1: ERC677 + let token2: ERC677 + let controller: SDLPoolCCIPControllerPrimary + let sdlPool: SDLPoolPrimary + let onRamp: CCIPOnRampMock + let offRamp: CCIPOffRampMock + let tokenPool: CCIPTokenPoolMock + let tokenPool2: CCIPTokenPoolMock + let router: any + let accounts: string[] + let signers: Signer[] + + before(async () => { + ;({ signers, accounts } = await getAccounts()) + }) + + beforeEach(async () => { + linkToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + sdlToken = (await deploy('ERC677', ['SDL', 'SDL', 1000000000])) as ERC677 + token1 = (await deploy('ERC677', ['2', '2', 1000000000])) as ERC677 + token2 = (await deploy('ERC677', ['2', '2', 1000000000])) as ERC677 + + const armProxy = await deploy('CCIPArmProxyMock') + router = (await deploy('Router', [accounts[0], armProxy.address])) as Router + tokenPool = (await deploy('CCIPTokenPoolMock', [token1.address])) as CCIPTokenPoolMock + tokenPool2 = (await deploy('CCIPTokenPoolMock', [token2.address])) as CCIPTokenPoolMock + onRamp = (await deploy('CCIPOnRampMock', [ + [token1.address, token2.address], + [tokenPool.address, tokenPool2.address], + linkToken.address, + ])) as CCIPOnRampMock + offRamp = (await deploy('CCIPOffRampMock', [ + router.address, + [token1.address, token2.address], + [tokenPool.address, tokenPool2.address], + ])) as CCIPOffRampMock + + await router.applyRampUpdates([[77, onRamp.address]], [], [[77, offRamp.address]]) + + let boostController = await deploy('LinearBoostController', [4 * 365 * 86400, 4]) + sdlPool = (await deployUpgradeable('SDLPoolPrimary', [ + 'reSDL', + 'reSDL', + sdlToken.address, + boostController.address, + ])) as SDLPoolPrimary + controller = (await deploy('SDLPoolCCIPControllerPrimary', [ + router.address, + linkToken.address, + sdlToken.address, + sdlPool.address, + toEther(10), + ])) as SDLPoolCCIPControllerPrimary + + await linkToken.transfer(controller.address, toEther(100)) + await sdlToken.transfer(accounts[1], toEther(200)) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) + ) + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + + await sdlPool.setCCIPController(controller.address) + await controller.setRESDLTokenBridge(accounts[5]) + }) + + it('handleOutgoingRESDL should work correctly', async () => { + await sdlToken.transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) + ) + let ts = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp + + await expect( + controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[1], 3) + ).to.be.revertedWith('SenderNotAuthorized()') + + assert.deepEqual( + parseLock( + await controller.connect(signers[5]).callStatic.handleOutgoingRESDL(77, accounts[0], 3) + ), + { amount: 200, boostAmount: 200, startTime: ts, duration: 365 * 86400, expiry: 0 } + ) + + await controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[0], 3) + assert.equal(fromEther(await sdlToken.balanceOf(accounts[5])), 200) + assert.equal(fromEther(await controller.reSDLSupplyByChain(77)), 400) + await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') + }) + + it('handleIncomingRESDL should work correctly', async () => { + await sdlToken.connect(signers[5]).approve(controller.address, toEther(100)) + await controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[0], 1) + + await controller + .connect(signers[5]) + .handleIncomingRESDL(77, accounts[3], 1, toEther(100), toEther(100), 111, 222, 0) + assert.equal(fromEther(await sdlToken.balanceOf(accounts[5])), 0) + assert.equal(fromEther(await controller.reSDLSupplyByChain(77)), 0) + assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 300) + assert.equal(await sdlPool.ownerOf(1), accounts[3]) + assert.deepEqual(parseLock((await sdlPool.getLocks([1]))[0]), { + amount: 100, + boostAmount: 100, + startTime: 111, + duration: 222, + expiry: 0, + }) + }) + + it('adding/removing whitelisted chains should work correctly', async () => { + await controller.addWhitelistedChain(77, accounts[5], '0x11') + await controller.addWhitelistedChain(88, accounts[6], '0x22') + + assert.deepEqual( + (await controller.getWhitelistedChains()).map((d) => d.toNumber()), + [77, 88] + ) + assert.equal(await controller.whitelistedDestinations(77), accounts[5]) + assert.equal(await controller.whitelistedDestinations(88), accounts[6]) + assert.equal(await controller.extraArgsByChain(77), '0x11') + assert.equal(await controller.extraArgsByChain(88), '0x22') + + await expect(controller.addWhitelistedChain(77, accounts[7], '0x11')).to.be.revertedWith( + 'AlreadyAdded()' + ) + await expect( + controller.addWhitelistedChain(99, ethers.constants.AddressZero, '0x11') + ).to.be.revertedWith('InvalidDestination()') + + await controller.removeWhitelistedChain(77) + assert.deepEqual( + (await controller.getWhitelistedChains()).map((d) => d.toNumber()), + [88] + ) + assert.equal(await controller.whitelistedDestinations(77), ethers.constants.AddressZero) + assert.equal(await controller.extraArgsByChain(77), '0x') + + await expect(controller.removeWhitelistedChain(77)).to.be.revertedWith('InvalidDestination()') + }) + + it('distributeRewards should work correctly', async () => { + let rewardsPool1 = await deploy('RewardsPool', [sdlPool.address, token1.address]) + await sdlPool.addToken(token1.address, rewardsPool1.address) + await controller.approveRewardTokens([token1.address, token2.address]) + await controller.addWhitelistedChain(77, accounts[6], '0x') + await controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[0], 1) + await token1.transferAndCall(rewardsPool1.address, toEther(50), '0x') + await controller.distributeRewards(['0x']) + + let requestData = await onRamp.getLastRequestData() + let requestMsg: any = await onRamp.getLastRequestMessage() + assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 98) + assert.equal(fromEther(requestData[0]), 2) + assert.equal(requestData[1], controller.address) + assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[6]) + assert.equal(requestMsg[3], linkToken.address) + assert.deepEqual( + requestMsg.tokenAmounts.map((d: any) => [d[0], fromEther(d[1])]), + [[token1.address, 25]] + ) + assert.equal(fromEther(await token1.balanceOf(tokenPool.address)), 25) + + let tokenPool88 = (await deploy('CCIPTokenPoolMock', [token1.address])) as CCIPTokenPoolMock + let tokenPool288 = (await deploy('CCIPTokenPoolMock', [token2.address])) as CCIPTokenPoolMock + let onRamp88 = (await deploy('CCIPOnRampMock', [ + [token1.address, token2.address], + [tokenPool88.address, tokenPool288.address], + linkToken.address, + ])) as CCIPOnRampMock + let offRamp88 = (await deploy('CCIPOffRampMock', [ + router.address, + [token1.address, token2.address], + [tokenPool88.address, tokenPool288.address], + ])) as CCIPOffRampMock + await router.applyRampUpdates([[88, onRamp88.address]], [], [[88, offRamp88.address]]) + + let rewardsPool2 = await deploy('RewardsPool', [sdlPool.address, token2.address]) + await sdlPool.addToken(token2.address, rewardsPool2.address) + await controller.addWhitelistedChain(88, accounts[7], '0x') + await sdlToken.transferAndCall( + sdlPool.address, + toEther(400), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await controller.connect(signers[5]).handleOutgoingRESDL(88, accounts[0], 3) + await token1.transferAndCall(rewardsPool1.address, toEther(200), '0x') + await token2.transferAndCall(rewardsPool2.address, toEther(300), '0x') + await controller.distributeRewards(['0x', '0x']) + + requestData = await onRamp.getLastRequestData() + requestMsg = await onRamp.getLastRequestMessage() + assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 94) + assert.equal(fromEther(requestData[0]), 2) + assert.equal(requestData[1], controller.address) + assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[6]) + assert.equal(requestMsg[3], linkToken.address) + assert.deepEqual( + requestMsg.tokenAmounts.map((d: any) => [d[0], fromEther(d[1])]), + [ + [token1.address, 50], + [token2.address, 75], + ] + ) + assert.equal(fromEther(await token1.balanceOf(tokenPool.address)), 75) + assert.equal(fromEther(await token2.balanceOf(tokenPool2.address)), 75) + + requestData = await onRamp88.getLastRequestData() + requestMsg = await onRamp88.getLastRequestMessage() + assert.equal(fromEther(requestData[0]), 2) + assert.equal(requestData[1], controller.address) + assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[7]) + assert.equal(requestMsg[3], linkToken.address) + assert.deepEqual( + requestMsg.tokenAmounts.map((d: any) => [d[0], fromEther(d[1])]), + [ + [token1.address, 100], + [token2.address, 150], + ] + ) + assert.equal(fromEther(await token1.balanceOf(tokenPool88.address)), 100) + assert.equal(fromEther(await token2.balanceOf(tokenPool288.address)), 150) + }) + + it('distributeRewards should work correctly with wrapped tokens', async () => { + let wToken = await deploy('WrappedSDTokenMock', [token1.address]) + let rewardsPool = await deploy('RewardsPoolWSD', [ + sdlPool.address, + token1.address, + wToken.address, + ]) + let wtokenPool = (await deploy('CCIPTokenPoolMock', [wToken.address])) as CCIPTokenPoolMock + await sdlPool.addToken(token1.address, rewardsPool.address) + await controller.approveRewardTokens([wToken.address]) + await controller.setWrappedRewardToken(token1.address, wToken.address) + await controller.addWhitelistedChain(77, accounts[6], '0x') + await onRamp.setTokenPool(wToken.address, wtokenPool.address) + await offRamp.setTokenPool(wToken.address, wtokenPool.address) + await controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[0], 1) + await token1.transferAndCall(rewardsPool.address, toEther(500), '0x') + await controller.distributeRewards(['0x']) + + let requestData = await onRamp.getLastRequestData() + let requestMsg: any = await onRamp.getLastRequestMessage() + assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 98) + assert.equal(fromEther(requestData[0]), 2) + assert.equal(requestData[1], controller.address) + assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[6]) + assert.equal(requestMsg[3], linkToken.address) + assert.deepEqual( + requestMsg.tokenAmounts.map((d: any) => [d[0], fromEther(d[1])]), + [[wToken.address, 125]] + ) + assert.equal(fromEther(await wToken.balanceOf(wtokenPool.address)), 125) + }) + + it('ccipReceive should work correctly', async () => { + await controller.addWhitelistedChain(77, accounts[5], '0x') + await offRamp + .connect(signers[5]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + ethers.utils.defaultAbiCoder.encode(['uint256', 'int256'], [3, toEther(1000)]), + controller.address, + [] + ) + + assert.equal((await sdlPool.lastLockId()).toNumber(), 5) + assert.equal(fromEther(await controller.reSDLSupplyByChain(77)), 1000) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(controller.address)), 1000) + + let requestData = await onRamp.getLastRequestData() + let requestMsg: any = await onRamp.getLastRequestMessage() + assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 98) + assert.equal(fromEther(requestData[0]), 2) + assert.equal(requestData[1], controller.address) + assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[5]) + assert.equal(ethers.utils.defaultAbiCoder.decode(['uint256'], requestMsg[1])[0], 3) + assert.equal(requestMsg[3], linkToken.address) + + await offRamp + .connect(signers[5]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + ethers.utils.defaultAbiCoder.encode(['uint256', 'int256'], [0, toEther(-100)]), + controller.address, + [] + ) + + assert.equal((await sdlPool.lastLockId()).toNumber(), 5) + assert.equal(fromEther(await controller.reSDLSupplyByChain(77)), 900) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(controller.address)), 900) + + requestData = await onRamp.getLastRequestData() + requestMsg = await onRamp.getLastRequestMessage() + assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 96) + assert.equal(fromEther(requestData[0]), 2) + assert.equal(requestData[1], controller.address) + assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[5]) + assert.equal(ethers.utils.defaultAbiCoder.decode(['uint256'], requestMsg[1])[0], 0) + assert.equal(requestMsg[3], linkToken.address) + + await controller.addWhitelistedChain(88, accounts[6], '0x') + let onRamp88 = (await deploy('CCIPOnRampMock', [[], [], linkToken.address])) as CCIPOnRampMock + let offRamp88 = (await deploy('CCIPOffRampMock', [router.address, [], []])) as CCIPOffRampMock + await router.applyRampUpdates([[88, onRamp88.address]], [], [[88, offRamp88.address]]) + await offRamp88 + .connect(signers[6]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 88, + ethers.utils.defaultAbiCoder.encode(['uint256', 'int256'], [2, toEther(200)]), + controller.address, + [] + ) + + assert.equal((await sdlPool.lastLockId()).toNumber(), 7) + assert.equal(fromEther(await controller.reSDLSupplyByChain(88)), 200) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(controller.address)), 1100) + + requestData = await onRamp88.getLastRequestData() + requestMsg = await onRamp88.getLastRequestMessage() + assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 94) + assert.equal(fromEther(requestData[0]), 2) + assert.equal(requestData[1], controller.address) + assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[6]) + assert.equal(ethers.utils.defaultAbiCoder.decode(['uint256'], requestMsg[1])[0].toNumber(), 6) + assert.equal(requestMsg[3], linkToken.address) + }) + + it('recoverTokens should work correctly', async () => { + await linkToken.transfer(controller.address, toEther(1000)) + await sdlToken.transfer(controller.address, toEther(2000)) + await controller.recoverTokens([linkToken.address, sdlToken.address], accounts[3]) + + assert.equal(fromEther(await linkToken.balanceOf(accounts[3])), 1100) + assert.equal(fromEther(await sdlToken.balanceOf(accounts[3])), 2000) + }) +}) diff --git a/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts new file mode 100644 index 00000000..f4c027c1 --- /dev/null +++ b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts @@ -0,0 +1,332 @@ +import { ethers } from 'hardhat' +import { assert, expect } from 'chai' +import { toEther, deploy, deployUpgradeable, getAccounts, fromEther } from '../../utils/helpers' +import { + ERC677, + CCIPOnRampMock, + CCIPOffRampMock, + CCIPTokenPoolMock, + SDLPoolCCIPControllerSecondary, + SDLPoolSecondary, +} from '../../../typechain-types' +import { time } from '@nomicfoundation/hardhat-network-helpers' +import { Signer } from 'ethers' + +const parseLock = (lock: any) => ({ + amount: fromEther(lock[0]), + boostAmount: Number(fromEther(lock[1]).toFixed(4)), + startTime: lock[2].toNumber(), + duration: lock[3].toNumber(), + expiry: lock[4].toNumber(), +}) + +describe('SDLPoolCCIPControllerSecondary', () => { + let linkToken: ERC677 + let sdlToken: ERC677 + let token1: ERC677 + let token2: ERC677 + let controller: SDLPoolCCIPControllerSecondary + let sdlPool: SDLPoolSecondary + let onRamp: CCIPOnRampMock + let offRamp: CCIPOffRampMock + let tokenPool: CCIPTokenPoolMock + let tokenPool2: CCIPTokenPoolMock + let accounts: string[] + let signers: Signer[] + + before(async () => { + ;({ signers, accounts } = await getAccounts()) + }) + + beforeEach(async () => { + linkToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + sdlToken = (await deploy('ERC677', ['SDL', 'SDL', 1000000000])) as ERC677 + token1 = (await deploy('ERC677', ['2', '2', 1000000000])) as ERC677 + token2 = (await deploy('ERC677', ['2', '2', 1000000000])) as ERC677 + + const armProxy = await deploy('CCIPArmProxyMock') + const router = await deploy('Router', [accounts[0], armProxy.address]) + tokenPool = (await deploy('CCIPTokenPoolMock', [token1.address])) as CCIPTokenPoolMock + tokenPool2 = (await deploy('CCIPTokenPoolMock', [token2.address])) as CCIPTokenPoolMock + onRamp = (await deploy('CCIPOnRampMock', [ + [token1.address, token2.address], + [tokenPool.address, tokenPool2.address], + linkToken.address, + ])) as CCIPOnRampMock + offRamp = (await deploy('CCIPOffRampMock', [ + router.address, + [token1.address, token2.address], + [tokenPool.address, tokenPool2.address], + ])) as CCIPOffRampMock + + await router.applyRampUpdates([[77, onRamp.address]], [], [[77, offRamp.address]]) + + let boostController = await deploy('LinearBoostController', [4 * 365 * 86400, 4]) + sdlPool = (await deployUpgradeable('SDLPoolSecondary', [ + 'reSDL', + 'reSDL', + sdlToken.address, + boostController.address, + ])) as SDLPoolSecondary + controller = (await deploy('SDLPoolCCIPControllerSecondary', [ + router.address, + linkToken.address, + sdlToken.address, + sdlPool.address, + 77, + accounts[4], + toEther(10), + 10000, + '0x', + ])) as SDLPoolCCIPControllerSecondary + + await linkToken.transfer(controller.address, toEther(100)) + await sdlToken.transfer(accounts[1], toEther(200)) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) + ) + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(200), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlPool.setCCIPController(accounts[0]) + await sdlPool.handleOutgoingUpdate() + await sdlPool.handleIncomingUpdate(1) + await sdlPool.executeQueuedOperations([]) + await sdlPool.connect(signers[1]).executeQueuedOperations([]) + await sdlPool.setCCIPController(controller.address) + await controller.setRESDLTokenBridge(accounts[5]) + }) + + it('handleOutgoingRESDL should work correctly', async () => { + await expect( + controller.connect(signers[5]).handleOutgoingRESDL(accounts[0], 2) + ).to.be.revertedWith('SenderNotAuthorized()') + + assert.deepEqual( + parseLock( + await controller.connect(signers[5]).callStatic.handleOutgoingRESDL(accounts[1], 2) + ), + { amount: 200, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 } + ) + + await controller.connect(signers[5]).handleOutgoingRESDL(accounts[1], 2) + assert.equal(fromEther(await sdlToken.balanceOf(accounts[5])), 200) + await expect(sdlPool.ownerOf(2)).to.be.revertedWith('InvalidLockId()') + }) + + it('handleIncomingRESDL should work correctly', async () => { + await sdlToken.transfer(accounts[5], toEther(300)) + await sdlToken.connect(signers[5]).approve(controller.address, toEther(300)) + + await controller + .connect(signers[5]) + .handleIncomingRESDL(accounts[3], 7, toEther(300), toEther(200), 111, 222, 0) + assert.equal(fromEther(await sdlToken.balanceOf(accounts[5])), 0) + assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 600) + assert.equal(await sdlPool.ownerOf(7), accounts[3]) + assert.deepEqual(parseLock((await sdlPool.getLocks([7]))[0]), { + amount: 300, + boostAmount: 200, + startTime: 111, + duration: 222, + expiry: 0, + }) + }) + + it('checkUpkeep should work correctly', async () => { + assert.equal((await controller.checkUpkeep('0x'))[0], false) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) + ) + assert.equal((await controller.checkUpkeep('0x'))[0], true) + + await controller.performUpkeep('0x') + assert.equal((await controller.checkUpkeep('0x'))[0], false) + + await offRamp + .connect(signers[4]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + ethers.utils.defaultAbiCoder.encode(['uint256'], [3]), + controller.address, + [] + ) + assert.equal((await controller.checkUpkeep('0x'))[0], false) + + await sdlPool.connect(signers[1]).withdraw(2, toEther(10)) + assert.equal((await controller.checkUpkeep('0x'))[0], false) + + await time.increase(10000) + assert.equal((await controller.checkUpkeep('0x'))[0], true) + }) + + it('performUpkeep should work correctly', async () => { + await expect(controller.performUpkeep('0x')).to.be.revertedWith('UpdateConditionsNotMet()') + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) + ) + await controller.performUpkeep('0x') + await expect(controller.performUpkeep('0x')).to.be.revertedWith('UpdateConditionsNotMet()') + + let lastRequestData = await onRamp.getLastRequestData() + let lastRequestMsg = await onRamp.getLastRequestMessage() + + assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 98) + + assert.equal(fromEther(lastRequestData[0]), 2) + assert.equal(lastRequestData[1], controller.address) + + assert.equal( + ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], + accounts[4] + ) + assert.deepEqual( + ethers.utils.defaultAbiCoder + .decode(['uint256', 'int256'], lastRequestMsg[1]) + .map((d, i) => (i == 0 ? d.toNumber() : fromEther(d))), + [1, 200] + ) + assert.equal(lastRequestMsg[3], linkToken.address) + + await offRamp + .connect(signers[4]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + ethers.utils.defaultAbiCoder.encode(['uint256'], [3]), + controller.address, + [] + ) + await expect(controller.performUpkeep('0x')).to.be.revertedWith('UpdateConditionsNotMet()') + + await sdlPool.connect(signers[1]).withdraw(2, toEther(10)) + await expect(controller.performUpkeep('0x')).to.be.revertedWith('UpdateConditionsNotMet()') + + await time.increase(10000) + await controller.performUpkeep('0x') + + lastRequestData = await onRamp.getLastRequestData() + lastRequestMsg = await onRamp.getLastRequestMessage() + + assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 96) + + assert.equal(fromEther(lastRequestData[0]), 2) + assert.equal(lastRequestData[1], controller.address) + + assert.equal( + ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], + accounts[4] + ) + assert.deepEqual( + ethers.utils.defaultAbiCoder + .decode(['uint256', 'int256'], lastRequestMsg[1]) + .map((d, i) => (i == 0 ? d.toNumber() : fromEther(d))), + [0, -10] + ) + assert.equal(lastRequestMsg[3], linkToken.address) + }) + + it('ccipReceive should work correctly for reward distributions', async () => { + await token1.transfer(tokenPool.address, toEther(1000)) + await token2.transfer(tokenPool2.address, toEther(1000)) + let rewardsPool1 = await deploy('RewardsPool', [sdlPool.address, token1.address]) + await sdlPool.addToken(token1.address, rewardsPool1.address) + + await offRamp + .connect(signers[4]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + '0x', + controller.address, + [ + { token: token1.address, amount: toEther(25) }, + { token: token2.address, amount: toEther(50) }, + ] + ) + + let events: any = await controller.queryFilter( + controller.filters['MessageFailed(bytes32,bytes)']() + ) + assert.equal(events[0].args.messageId, ethers.utils.formatBytes32String('messageId')) + assert.equal(fromEther(await token1.balanceOf(controller.address)), 25) + assert.equal(fromEther(await token2.balanceOf(controller.address)), 50) + + let rewardsPool2 = await deploy('RewardsPool', [sdlPool.address, token2.address]) + await sdlPool.addToken(token2.address, rewardsPool2.address) + + await offRamp + .connect(signers[4]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + '0x', + controller.address, + [ + { token: token1.address, amount: toEther(30) }, + { token: token2.address, amount: toEther(60) }, + ] + ) + + assert.equal(fromEther(await token1.balanceOf(rewardsPool1.address)), 30) + assert.equal(fromEther(await token2.balanceOf(rewardsPool2.address)), 60) + assert.deepEqual( + (await sdlPool.withdrawableRewards(accounts[0])).map((d) => fromEther(d)), + [15, 30] + ) + assert.deepEqual( + (await sdlPool.withdrawableRewards(accounts[1])).map((d) => fromEther(d)), + [15, 30] + ) + }) + + it('ccipReceive should work correctly for incoming updates', async () => { + await sdlToken.transferAndCall( + sdlPool.address, + toEther(300), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) + ) + await controller.performUpkeep('0x') + await offRamp + .connect(signers[4]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + ethers.utils.defaultAbiCoder.encode(['uint256'], [7]), + controller.address, + [] + ) + await sdlPool.executeQueuedOperations([]) + + assert.deepEqual(parseLock((await sdlPool.getLocks([7]))[0]), { + amount: 300, + boostAmount: 100, + startTime: 0, + duration: 0, + expiry: 0, + }) + assert.equal(await sdlPool.shouldUpdate(), false) + }) + + it('recoverTokens should work correctly', async () => { + await linkToken.transfer(controller.address, toEther(1000)) + await sdlToken.transfer(controller.address, toEther(2000)) + await controller.recoverTokens([linkToken.address, sdlToken.address], accounts[3]) + + assert.equal(fromEther(await linkToken.balanceOf(accounts[3])), 1100) + assert.equal(fromEther(await sdlToken.balanceOf(accounts[3])), 2000) + }) +}) diff --git a/test/core/ccip/wrapped-token-bridge.test.ts b/test/core/ccip/wrapped-token-bridge.test.ts index 3d49fe99..a9ca63ee 100644 --- a/test/core/ccip/wrapped-token-bridge.test.ts +++ b/test/core/ccip/wrapped-token-bridge.test.ts @@ -103,7 +103,7 @@ describe('WrappedTokenBridge', () => { let preFeeBalance = await linkToken.balanceOf(accounts[0]) await bridge.transferTokens(77, accounts[4], toEther(100), false, toEther(10), '0x') - let lastRequestData = await onRamp.lastRequestData() + let lastRequestData = await onRamp.getLastRequestData() let lastRequestMsg = await onRamp.getLastRequestMessage() assert.equal(fromEther(await wrappedToken.balanceOf(tokenPool.address)), 50) @@ -134,7 +134,7 @@ describe('WrappedTokenBridge', () => { await bridge.transferTokens(77, accounts[4], toEther(100), true, 0, '0x', { value: toEther(10), }) - let lastRequestData = await onRamp.lastRequestData() + let lastRequestData = await onRamp.getLastRequestData() let lastRequestMsg = await onRamp.getLastRequestMessage() assert.equal(fromEther(await wrappedToken.balanceOf(tokenPool.address)), 50) @@ -170,7 +170,7 @@ describe('WrappedTokenBridge', () => { ) ) - let lastRequestData = await onRamp.lastRequestData() + let lastRequestData = await onRamp.getLastRequestData() let lastRequestMsg = await onRamp.getLastRequestMessage() assert.equal(fromEther(await wrappedToken.balanceOf(tokenPool.address)), 50) From 793615b8f3681cf6344f4e76536fa426c54c8463 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Fri, 17 Nov 2023 09:02:19 -0500 Subject: [PATCH 16/42] fixed failing tests --- test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts index f4c027c1..8fa25a48 100644 --- a/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts +++ b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts @@ -297,7 +297,7 @@ describe('SDLPoolCCIPControllerSecondary', () => { await sdlToken.transferAndCall( sdlPool.address, toEther(300), - ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) ) await controller.performUpkeep('0x') await offRamp @@ -313,7 +313,7 @@ describe('SDLPoolCCIPControllerSecondary', () => { assert.deepEqual(parseLock((await sdlPool.getLocks([7]))[0]), { amount: 300, - boostAmount: 100, + boostAmount: 0, startTime: 0, duration: 0, expiry: 0, From 4cea0f6c04c2c0bf889b9679b9f662d288f5956e Mon Sep 17 00:00:00 2001 From: BkChoy Date: Fri, 17 Nov 2023 09:08:07 -0500 Subject: [PATCH 17/42] updated hardhat config --- hardhat.config.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/hardhat.config.ts b/hardhat.config.ts index 1d2128f9..20c82185 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -62,6 +62,15 @@ const config: HardhatUserConfig = { }, solidity: { compilers: [ + { + version: '0.8.19', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, { version: '0.8.15', settings: { From ccc34581cd9224baad5c73542d4554d6bd3ba6a6 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Fri, 17 Nov 2023 14:10:37 -0500 Subject: [PATCH 18/42] added extra args setter --- .../core/ccip/SDLPoolCCIPControllerSecondary.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol index 2e3e0d54..a00a5be9 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol @@ -22,6 +22,8 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { address public immutable primaryChainDestination; bytes public extraArgs; + event SetExtraArgs(bytes extraArgs); + error UpdateConditionsNotMet(); /** @@ -137,6 +139,15 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { timeBetweenUpdates = _timeBetweenUpdates; } + /** + * @notice Sets the extra args for sending updates to the primary chain + * @param _extraArgs extra args as defined in CCIP API + **/ + function setExtraArgs(bytes calldata _extraArgs) external onlyOwner { + extraArgs = _extraArgs; + emit SetExtraArgs(_extraArgs); + } + /** * @notice Initiates an update to the primary chain * @param _destinationChainSelector id of destination chain From 67fe1746dadf4fc603e091f35265ccc19a929663 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 20 Nov 2023 14:56:42 -0500 Subject: [PATCH 19/42] fixed bridge interface issue --- contracts/core/ccip/RESDLTokenBridge.sol | 18 +++++++++++++----- .../ccip/SDLPoolCCIPControllerSecondary.sol | 7 ++++++- .../core/interfaces/ISDLPoolCCIPController.sol | 7 ++++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/contracts/core/ccip/RESDLTokenBridge.sol b/contracts/core/ccip/RESDLTokenBridge.sol index 2d61e21f..d0473ffe 100644 --- a/contracts/core/ccip/RESDLTokenBridge.sol +++ b/contracts/core/ccip/RESDLTokenBridge.sol @@ -113,13 +113,12 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { if (sender != sdlPool.ownerOf(_tokenId)) revert SenderNotAuthorized(); if (_receiver == address(0)) revert InvalidReceiver(); - address destination = whitelistedDestinations[_destinationChainSelector]; - if (destination == address(0)) revert InvalidDestination(); + if (whitelistedDestinations[_destinationChainSelector] == address(0)) revert InvalidDestination(); RESDLToken memory reSDLToken; { (uint256 amount, uint256 boostAmount, uint64 startTime, uint64 duration, uint64 expiry) = sdlPoolCCIPController - .handleOutgoingRESDL(sender, _tokenId); + .handleOutgoingRESDL(_destinationChainSelector, sender, _tokenId); reSDLToken = RESDLToken(amount, boostAmount, startTime, duration, expiry); } @@ -127,7 +126,7 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { _receiver, _tokenId, reSDLToken, - destination, + whitelistedDestinations[_destinationChainSelector], _payNative ? address(0) : address(linkToken), _extraArgs ); @@ -298,7 +297,16 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { uint64 expiry ) = abi.decode(_any2EvmMessage.data, (address, uint256, uint256, uint256, uint64, uint64, uint64)); - sdlPoolCCIPController.handleIncomingRESDL(receiver, tokenId, amount, boostAmount, startTime, duration, expiry); + sdlPoolCCIPController.handleIncomingRESDL( + _any2EvmMessage.sourceChainSelector, + receiver, + tokenId, + amount, + boostAmount, + startTime, + duration, + expiry + ); emit TokenReceived(_any2EvmMessage.messageId, _any2EvmMessage.sourceChainSelector, sender, receiver, tokenId); } diff --git a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol index a00a5be9..92789172 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol @@ -86,7 +86,11 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { * @param _tokenId id of token * @return the token being transferred **/ - function handleOutgoingRESDL(address _sender, uint256 _tokenId) + function handleOutgoingRESDL( + uint64, + address _sender, + uint256 _tokenId + ) external onlyBridge returns ( @@ -111,6 +115,7 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { * @param _expiry expiry time of the lock **/ function handleIncomingRESDL( + uint64, address _receiver, uint256 _tokenId, uint256 _amount, diff --git a/contracts/core/interfaces/ISDLPoolCCIPController.sol b/contracts/core/interfaces/ISDLPoolCCIPController.sol index 467a2211..6e3a9672 100644 --- a/contracts/core/interfaces/ISDLPoolCCIPController.sol +++ b/contracts/core/interfaces/ISDLPoolCCIPController.sol @@ -2,7 +2,11 @@ pragma solidity 0.8.15; interface ISDLPoolCCIPController { - function handleOutgoingRESDL(address _sender, uint256 _lockId) + function handleOutgoingRESDL( + uint64 _destinationChainSelector, + address _sender, + uint256 _lockId + ) external returns ( uint256 _amount, @@ -13,6 +17,7 @@ interface ISDLPoolCCIPController { ); function handleIncomingRESDL( + uint64 _sourceChainSelector, address _receiver, uint256 _lockId, uint256 _amount, From e2b87a88d22e8bc473b4321db05cdb9da89e2362 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Tue, 21 Nov 2023 09:44:07 -0500 Subject: [PATCH 20/42] allow ccipReceive to revert --- contracts/core/ccip/RESDLTokenBridge.sol | 37 ------------ contracts/core/ccip/WrappedTokenBridge.sol | 56 ------------------- .../core/ccip/base/SDLPoolCCIPController.sol | 25 --------- 3 files changed, 118 deletions(-) diff --git a/contracts/core/ccip/RESDLTokenBridge.sol b/contracts/core/ccip/RESDLTokenBridge.sol index d0473ffe..5c44b536 100644 --- a/contracts/core/ccip/RESDLTokenBridge.sol +++ b/contracts/core/ccip/RESDLTokenBridge.sol @@ -49,25 +49,18 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { address receiver, uint256 tokenId ); - event MessageFailed(bytes32 indexed messageId, bytes error); event DestinationAdded(uint64 indexed destinationChainSelector, address destination); event DestinationRemoved(uint64 indexed destinationChainSelector, address destination); error InsufficientFee(); error TransferFailed(); error FeeExceedsLimit(); - error OnlySelf(); error SenderNotAuthorized(); error InvalidDestination(); error InvalidReceiver(); error AlreadyAdded(); error AlreadyRemoved(); - modifier onlySelf() { - if (msg.sender != address(this)) revert OnlySelf(); - _; - } - /** * @notice Initializes the contract * @param _router address of the CCIP router @@ -182,18 +175,6 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { return IRouterClient(this.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); } - /** - * @notice Recovers tokens that were accidentally sent to this contract - * @param _tokens list of tokens to recover - * @param _receiver address to receive recovered tokens - **/ - function recoverTokens(address[] calldata _tokens, address _receiver) external onlyOwner { - for (uint256 i = 0; i < _tokens.length; ++i) { - IERC20 tokenToTransfer = IERC20(_tokens[i]); - tokenToTransfer.safeTransfer(_receiver, tokenToTransfer.balanceOf(address(this))); - } - } - /** * @notice Whitelists a new destination chain * @param _destinationChainSelector id of destination chain @@ -216,24 +197,6 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { delete whitelistedDestinations[_destinationChainSelector]; } - /** - * @notice Called by the CCIP router to deliver a message - * @param _any2EvmMessage CCIP message - **/ - function ccipReceive(Client.Any2EVMMessage calldata _any2EvmMessage) external override onlyRouter { - try this.processMessage(_any2EvmMessage) {} catch (bytes memory err) { - emit MessageFailed(_any2EvmMessage.messageId, err); - } - } - - /** - * @notice Processes a received message - * @param _any2EvmMessage CCIP message - **/ - function processMessage(Client.Any2EVMMessage calldata _any2EvmMessage) external onlySelf { - _ccipReceive(_any2EvmMessage); - } - /** * @notice Builds a CCIP message * @dev builds the message for outgoing reSDL transfers diff --git a/contracts/core/ccip/WrappedTokenBridge.sol b/contracts/core/ccip/WrappedTokenBridge.sol index 5b32551a..78a196c8 100644 --- a/contracts/core/ccip/WrappedTokenBridge.sol +++ b/contracts/core/ccip/WrappedTokenBridge.sol @@ -19,19 +19,11 @@ import "../interfaces/IWrappedLST.sol"; contract WrappedTokenBridge is Ownable, CCIPReceiver { using SafeERC20 for IERC20; - enum ErrorStatus { - RESOLVED, - UNRESOLVED - } - IERC20 linkToken; IERC20 token; IWrappedLST wrappedToken; - mapping(bytes32 => ErrorStatus) public messageErrorsStatus; - mapping(bytes32 => Client.Any2EVMMessage) public failedMessages; - event TokensTransferred( bytes32 indexed messageId, uint64 indexed destinationChainSelector, @@ -48,23 +40,14 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { address receiver, uint256 tokenAmount ); - event MessageFailed(bytes32 indexed messageId, bytes error); - event MessageResolved(bytes32 indexed messageId); error InvalidSender(); error InvalidValue(); error InsufficientFee(); error TransferFailed(); error FeeExceedsLimit(); - error OnlySelf(); - error MessageIsResolved(); error InvalidMessage(); - modifier onlySelf() { - if (msg.sender != address(this)) revert OnlySelf(); - _; - } - /** * @notice Initializes the contract * @param _router address of the CCIP router @@ -154,45 +137,6 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { return IRouterClient(this.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); } - /** - * @notice Called by the CCIP router to deliver a message - * @param _any2EvmMessage CCIP message - **/ - function ccipReceive(Client.Any2EVMMessage calldata _any2EvmMessage) external override onlyRouter { - try this.processMessage(_any2EvmMessage) {} catch (bytes memory err) { - bytes32 messageId = _any2EvmMessage.messageId; - messageErrorsStatus[messageId] = ErrorStatus.UNRESOLVED; - failedMessages[messageId] = _any2EvmMessage; - emit MessageFailed(messageId, err); - } - } - - /** - * @notice Processes a received message - * @param _any2EvmMessage CCIP message - **/ - function processMessage(Client.Any2EVMMessage calldata _any2EvmMessage) external onlySelf { - _ccipReceive(_any2EvmMessage); - } - - /** - * @notice Executes a failed message - * @param _messageId id of CCIP message - * @param _tokenReceiver address to receive all token transfers included in the message - **/ - function retryFailedMessage(bytes32 _messageId, address _tokenReceiver) external onlyOwner { - if (messageErrorsStatus[_messageId] != ErrorStatus.UNRESOLVED) revert MessageIsResolved(); - - messageErrorsStatus[_messageId] = ErrorStatus.RESOLVED; - - Client.Any2EVMMessage memory message = failedMessages[_messageId]; - for (uint256 i = 0; i < message.destTokenAmounts.length; ++i) { - IERC20(message.destTokenAmounts[i].token).safeTransfer(_tokenReceiver, message.destTokenAmounts[i].amount); - } - - emit MessageResolved(_messageId); - } - /** * @notice Recovers tokens that were accidentally sent to this contract * @param _tokens list of tokens to recover diff --git a/contracts/core/ccip/base/SDLPoolCCIPController.sol b/contracts/core/ccip/base/SDLPoolCCIPController.sol index b984cdc9..985408f3 100644 --- a/contracts/core/ccip/base/SDLPoolCCIPController.sol +++ b/contracts/core/ccip/base/SDLPoolCCIPController.sol @@ -20,20 +20,13 @@ abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { event MessageSent(bytes32 indexed messageId, uint64 indexed destinationChainSelector, uint256 fees); event MessageReceived(bytes32 indexed messageId, uint64 indexed destinationChainSelector); - event MessageFailed(bytes32 indexed messageId, bytes error); - error OnlySelf(); error OnlyRESDLTokenBridge(); error AlreadyAdded(); error InvalidDestination(); error SenderNotAuthorized(); error FeeExceedsLimit(uint256 fee); - modifier onlySelf() { - if (msg.sender != address(this)) revert OnlySelf(); - _; - } - modifier onlyBridge() { if (msg.sender != reSDLTokenBridge) revert OnlyRESDLTokenBridge(); _; @@ -88,22 +81,4 @@ abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { function setRESDLTokenBridge(address _reSDLTokenBridge) external onlyOwner { reSDLTokenBridge = _reSDLTokenBridge; } - - /** - * @notice Called by the CCIP router to deliver a message - * @param _any2EvmMessage CCIP message - **/ - function ccipReceive(Client.Any2EVMMessage calldata _any2EvmMessage) external override onlyRouter { - try this.processMessage(_any2EvmMessage) {} catch (bytes memory err) { - emit MessageFailed(_any2EvmMessage.messageId, err); - } - } - - /** - * @notice Processes a received message - * @param _any2EvmMessage CCIP message - **/ - function processMessage(Client.Any2EVMMessage calldata _any2EvmMessage) external onlySelf { - _ccipReceive(_any2EvmMessage); - } } From 0867ae249f237a1c7114596c29d19b4d58867223 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Tue, 21 Nov 2023 10:46:34 -0500 Subject: [PATCH 21/42] fixed tests --- .../core/test/SDLPoolCCIPControllerMock.sol | 8 +++- .../core/test/chainlink/CCIPOffRampMock.sol | 5 +- test/core/ccip/resdl-token-bridge.test.ts | 20 ++------ ...sdl-pool-ccip-controller-secondary.test.ts | 43 ++++++++++++----- test/core/ccip/wrapped-token-bridge.test.ts | 47 ++++++------------- 5 files changed, 57 insertions(+), 66 deletions(-) diff --git a/contracts/core/test/SDLPoolCCIPControllerMock.sol b/contracts/core/test/SDLPoolCCIPControllerMock.sol index ad834029..021f7537 100644 --- a/contracts/core/test/SDLPoolCCIPControllerMock.sol +++ b/contracts/core/test/SDLPoolCCIPControllerMock.sol @@ -12,7 +12,6 @@ contract SDLPoolCCIPControllerMock { ISDLPool public sdlPool; address public reSDLTokenBridge; - error OnlySelf(); error OnlyRESDLTokenBridge(); modifier onlyBridge() { @@ -25,7 +24,11 @@ contract SDLPoolCCIPControllerMock { sdlPool = ISDLPool(_sdlPool); } - function handleOutgoingRESDL(address _sender, uint256 _tokenId) + function handleOutgoingRESDL( + uint64, + address _sender, + uint256 _tokenId + ) external onlyBridge returns ( @@ -40,6 +43,7 @@ contract SDLPoolCCIPControllerMock { } function handleIncomingRESDL( + uint64, address _receiver, uint256 _tokenId, uint256 _amount, diff --git a/contracts/core/test/chainlink/CCIPOffRampMock.sol b/contracts/core/test/chainlink/CCIPOffRampMock.sol index 33c654cf..eed6b8d4 100644 --- a/contracts/core/test/chainlink/CCIPOffRampMock.sol +++ b/contracts/core/test/chainlink/CCIPOffRampMock.sol @@ -37,17 +37,18 @@ contract CCIPOffRampMock { bytes calldata _data, address _receiver, Client.EVMTokenAmount[] calldata _tokenAmounts - ) external { + ) external returns (bool) { for (uint256 i = 0; i < _tokenAmounts.length; ++i) { tokenPools[_tokenAmounts[i].token].releaseOrMint(_receiver, _tokenAmounts[i].amount); } - router.routeMessage( + (bool success, ) = router.routeMessage( Client.Any2EVMMessage(_messageId, _sourceChainSelector, abi.encode(msg.sender), _data, _tokenAmounts), GAS_FOR_CALL_EXACT_CHECK, 1000000, _receiver ); + return success; } function setTokenPool(address _token, address _pool) external { diff --git a/test/core/ccip/resdl-token-bridge.test.ts b/test/core/ccip/resdl-token-bridge.test.ts index d19a1934..1e6ba8f5 100644 --- a/test/core/ccip/resdl-token-bridge.test.ts +++ b/test/core/ccip/resdl-token-bridge.test.ts @@ -190,7 +190,7 @@ describe('RESDLTokenBridge', () => { await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') }) - it('transferTokens should work correctly with native fee', async () => { + it('transferRESDL should work correctly with native fee', async () => { let ts = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp let preFeeBalance = await ethers.provider.getBalance(accounts[0]) @@ -249,9 +249,9 @@ describe('RESDLTokenBridge', () => { it('ccipReceive should work correctly', async () => { await bridge.transferRESDL(77, accounts[4], 2, true, toEther(10), '0x', { value: toEther(10) }) - await offRamp + let success: any = await offRamp .connect(signers[1]) - .executeSingleMessage( + .callStatic.executeSingleMessage( ethers.utils.formatBytes32String('messageId'), 77, ethers.utils.defaultAbiCoder.encode( @@ -261,10 +261,7 @@ describe('RESDLTokenBridge', () => { bridge.address, [{ token: sdlToken.address, amount: toEther(25) }] ) - - let events: any = await bridge.queryFilter(bridge.filters['MessageFailed(bytes32,bytes)']()) - assert.equal(events[0].args.messageId, ethers.utils.formatBytes32String('messageId')) - assert.equal(fromEther(await sdlToken.balanceOf(bridge.address)), 25) + assert.equal(success, false) await offRamp.executeSingleMessage( ethers.utils.formatBytes32String('messageId'), @@ -315,13 +312,4 @@ describe('RESDLTokenBridge', () => { await bridge.removeWhitelistedDestination(10) assert.equal(await bridge.whitelistedDestinations(10), ethers.constants.AddressZero) }) - - it('recoverTokens should work correctly', async () => { - await linkToken.transfer(bridge.address, toEther(1000)) - await sdlToken.transfer(bridge.address, toEther(2000)) - await bridge.recoverTokens([linkToken.address, sdlToken.address], accounts[3]) - - assert.equal(fromEther(await linkToken.balanceOf(accounts[3])), 1000) - assert.equal(fromEther(await sdlToken.balanceOf(accounts[3])), 2000) - }) }) diff --git a/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts index 8fa25a48..981010bc 100644 --- a/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts +++ b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts @@ -25,7 +25,7 @@ describe('SDLPoolCCIPControllerSecondary', () => { let sdlToken: ERC677 let token1: ERC677 let token2: ERC677 - let controller: SDLPoolCCIPControllerSecondary + let controller: any let sdlPool: SDLPoolSecondary let onRamp: CCIPOnRampMock let offRamp: CCIPOffRampMock @@ -105,17 +105,17 @@ describe('SDLPoolCCIPControllerSecondary', () => { it('handleOutgoingRESDL should work correctly', async () => { await expect( - controller.connect(signers[5]).handleOutgoingRESDL(accounts[0], 2) + controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[0], 2) ).to.be.revertedWith('SenderNotAuthorized()') assert.deepEqual( parseLock( - await controller.connect(signers[5]).callStatic.handleOutgoingRESDL(accounts[1], 2) + await controller.connect(signers[5]).callStatic.handleOutgoingRESDL(77, accounts[1], 2) ), { amount: 200, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 } ) - await controller.connect(signers[5]).handleOutgoingRESDL(accounts[1], 2) + await controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[1], 2) assert.equal(fromEther(await sdlToken.balanceOf(accounts[5])), 200) await expect(sdlPool.ownerOf(2)).to.be.revertedWith('InvalidLockId()') }) @@ -126,7 +126,7 @@ describe('SDLPoolCCIPControllerSecondary', () => { await controller .connect(signers[5]) - .handleIncomingRESDL(accounts[3], 7, toEther(300), toEther(200), 111, 222, 0) + .handleIncomingRESDL(77, accounts[3], 7, toEther(300), toEther(200), 111, 222, 0) assert.equal(fromEther(await sdlToken.balanceOf(accounts[5])), 0) assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 600) assert.equal(await sdlPool.ownerOf(7), accounts[3]) @@ -245,9 +245,9 @@ describe('SDLPoolCCIPControllerSecondary', () => { let rewardsPool1 = await deploy('RewardsPool', [sdlPool.address, token1.address]) await sdlPool.addToken(token1.address, rewardsPool1.address) - await offRamp + let success: any = await offRamp .connect(signers[4]) - .executeSingleMessage( + .callStatic.executeSingleMessage( ethers.utils.formatBytes32String('messageId'), 77, '0x', @@ -257,13 +257,18 @@ describe('SDLPoolCCIPControllerSecondary', () => { { token: token2.address, amount: toEther(50) }, ] ) + assert.equal(success, false) - let events: any = await controller.queryFilter( - controller.filters['MessageFailed(bytes32,bytes)']() - ) - assert.equal(events[0].args.messageId, ethers.utils.formatBytes32String('messageId')) - assert.equal(fromEther(await token1.balanceOf(controller.address)), 25) - assert.equal(fromEther(await token2.balanceOf(controller.address)), 50) + success = await offRamp + .connect(signers[5]) + .callStatic.executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + '0x', + controller.address, + [{ token: token1.address, amount: toEther(25) }] + ) + assert.equal(success, false) let rewardsPool2 = await deploy('RewardsPool', [sdlPool.address, token2.address]) await sdlPool.addToken(token2.address, rewardsPool2.address) @@ -300,6 +305,18 @@ describe('SDLPoolCCIPControllerSecondary', () => { ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) ) await controller.performUpkeep('0x') + + let success: any = await offRamp + .connect(signers[5]) + .callStatic.executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + ethers.utils.defaultAbiCoder.encode(['uint256'], [7]), + controller.address, + [] + ) + assert.equal(success, false) + await offRamp .connect(signers[4]) .executeSingleMessage( diff --git a/test/core/ccip/wrapped-token-bridge.test.ts b/test/core/ccip/wrapped-token-bridge.test.ts index a9ca63ee..31dd520c 100644 --- a/test/core/ccip/wrapped-token-bridge.test.ts +++ b/test/core/ccip/wrapped-token-bridge.test.ts @@ -226,57 +226,38 @@ describe('WrappedTokenBridge', () => { ) assert.equal(fromEther(await stakingPool.balanceOf(accounts[5])), 50) - }) - it('failed messages should be properly handled', async () => { - await stakingPool.transferAndCall( - bridge.address, - toEther(100), - ethers.utils.defaultAbiCoder.encode( - ['uint64', 'address', 'uint256', 'bytes'], - [77, accounts[4], toEther(10), '0x'] - ) - ) await token2.transfer(tokenPool2.address, toEther(100)) - await offRamp.executeSingleMessage( - ethers.utils.formatBytes32String('messageId1'), + + let success: any = await offRamp.callStatic.executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), 77, ethers.utils.defaultAbiCoder.encode(['address'], [accounts[5]]), bridge.address, [ { token: wrappedToken.address, amount: toEther(25) }, - { token: token2.address, amount: toEther(10) }, + { token: token2.address, amount: toEther(25) }, ] ) - await offRamp.executeSingleMessage( - ethers.utils.formatBytes32String('messageId2'), + assert.equal(success, false) + + success = await offRamp.callStatic.executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), 77, ethers.utils.defaultAbiCoder.encode(['address'], [accounts[5]]), bridge.address, - [{ token: token2.address, amount: toEther(10) }] + [{ token: token2.address, amount: toEther(25) }] ) - await offRamp.executeSingleMessage( - ethers.utils.formatBytes32String('messageId3'), + assert.equal(success, false) + + success = await offRamp.callStatic.executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), 77, '0x', bridge.address, [{ token: wrappedToken.address, amount: toEther(25) }] ) - - let events: any = await bridge.queryFilter(bridge.filters['MessageFailed(bytes32,bytes)']()) - - await bridge.retryFailedMessage(events[1].args.messageId, accounts[4]) - assert.equal(await bridge.messageErrorsStatus(events[1].args.messageId), 0) - assert.equal(fromEther(await token2.balanceOf(accounts[4])), 10) - - await bridge.retryFailedMessage(events[2].args.messageId, accounts[5]) - assert.equal(await bridge.messageErrorsStatus(events[2].args.messageId), 0) - assert.equal(fromEther(await wrappedToken.balanceOf(accounts[5])), 25) - - await bridge.retryFailedMessage(events[0].args.messageId, accounts[6]) - assert.equal(await bridge.messageErrorsStatus(events[1].args.messageId), 0) - assert.equal(fromEther(await token2.balanceOf(accounts[6])), 10) - assert.equal(fromEther(await wrappedToken.balanceOf(accounts[6])), 25) + assert.equal(success, false) }) it('recoverTokens should work correctly', async () => { From 1b0f8b5cc56a3508094e5dc8026cdb21ac5d4060 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Tue, 5 Dec 2023 13:21:34 -0500 Subject: [PATCH 22/42] removed user ability to set extraArgs --- contracts/core/ccip/RESDLTokenBridge.sol | 32 ++++++----- .../ccip/SDLPoolCCIPControllerPrimary.sol | 54 ++++++++++++------- contracts/core/ccip/WrappedTokenBridge.sol | 38 +++++-------- 3 files changed, 66 insertions(+), 58 deletions(-) diff --git a/contracts/core/ccip/RESDLTokenBridge.sol b/contracts/core/ccip/RESDLTokenBridge.sol index 5c44b536..5280f47b 100644 --- a/contracts/core/ccip/RESDLTokenBridge.sol +++ b/contracts/core/ccip/RESDLTokenBridge.sol @@ -33,6 +33,8 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { mapping(uint64 => address) public whitelistedDestinations; + bytes public extraArgs; + event TokenTransferred( bytes32 indexed messageId, uint64 indexed destinationChainSelector, @@ -51,6 +53,7 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { ); event DestinationAdded(uint64 indexed destinationChainSelector, address destination); event DestinationRemoved(uint64 indexed destinationChainSelector, address destination); + event SetExtraArgs(bytes extraArgs); error InsufficientFee(); error TransferFailed(); @@ -68,18 +71,21 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { * @param _sdlToken address of the SDL token * @param _sdlPool address of the SDL Pool * @param _sdlPoolCCIPController address of the SDL Pool CCIP controller + * @param _extraArgs encoded args as defined in CCIP API used for sending transfers **/ constructor( address _router, address _linkToken, address _sdlToken, address _sdlPool, - address _sdlPoolCCIPController + address _sdlPoolCCIPController, + bytes memory _extraArgs ) CCIPReceiver(_router) { linkToken = IERC20(_linkToken); sdlToken = IERC20(_sdlToken); sdlPool = ISDLPool(_sdlPool); sdlPoolCCIPController = ISDLPoolCCIPController(_sdlPoolCCIPController); + extraArgs = _extraArgs; linkToken.safeApprove(_router, type(uint256).max); sdlToken.safeApprove(_router, type(uint256).max); sdlToken.safeApprove(_sdlPoolCCIPController, type(uint256).max); @@ -92,15 +98,13 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { * @param _tokenId id of reSDL token * @param _payNative whether fee should be paid natively or with LINK * @param _maxLINKFee call will revert if LINK fee exceeds this value - * @param _extraArgs encoded args as defined in CCIP API **/ function transferRESDL( uint64 _destinationChainSelector, address _receiver, uint256 _tokenId, bool _payNative, - uint256 _maxLINKFee, - bytes memory _extraArgs + uint256 _maxLINKFee ) external payable returns (bytes32 messageId) { address sender = msg.sender; if (sender != sdlPool.ownerOf(_tokenId)) revert SenderNotAuthorized(); @@ -121,7 +125,7 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { reSDLToken, whitelistedDestinations[_destinationChainSelector], _payNative ? address(0) : address(linkToken), - _extraArgs + extraArgs ); IRouterClient router = IRouterClient(this.getRouter()); @@ -155,21 +159,16 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { * @notice Returns the current fee for an reSDL transfer * @param _destinationChainSelector id of destination chain * @param _payNative whether fee should be paid natively or with LINK - * @param _extraArgs encoded args as defined in CCIP API * @return fee current fee **/ - function getFee( - uint64 _destinationChainSelector, - bool _payNative, - bytes memory _extraArgs - ) external view returns (uint256) { + function getFee(uint64 _destinationChainSelector, bool _payNative) external view returns (uint256) { Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( address(this), 0, RESDLToken(0, 0, 0, 0, 0), address(this), _payNative ? address(0) : address(linkToken), - _extraArgs + extraArgs ); return IRouterClient(this.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); @@ -197,6 +196,15 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { delete whitelistedDestinations[_destinationChainSelector]; } + /** + * @notice sets extra args used for reSDL transfers + * @param _extraArgs encoded args as defined in CCIP API + */ + function setExtraArgs(bytes calldata _extraArgs) external onlyOwner { + extraArgs = _extraArgs; + emit SetExtraArgs(_extraArgs); + } + /** * @notice Builds a CCIP message * @dev builds the message for outgoing reSDL transfers diff --git a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol index ecdb594e..d78b46e3 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol @@ -15,15 +15,17 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { uint64[] internal whitelistedChains; mapping(uint64 => address) public whitelistedDestinations; - mapping(uint64 => bytes) public extraArgsByChain; + mapping(uint64 => bytes) public updateExtraArgsByChain; + mapping(uint64 => bytes) public rewardsExtraArgsByChain; mapping(uint64 => uint256) public reSDLSupplyByChain; mapping(address => address) public wrappedRewardTokens; event DistributeRewards(bytes32 indexed messageId, uint64 indexed destinationChainSelector, uint256 fees); - event ChainAdded(uint64 indexed chainSelector, address destination, bytes extraArgs); + event ChainAdded(uint64 indexed chainSelector, address destination, bytes updateExtraArgs, bytes rewardsExtraArgs); event ChainRemoved(uint64 indexed destinationChainSelector, address destination); - event SetExtraArgs(uint64 indexed chainSelector, bytes extraArgs); + event SetUpdateExtraArgs(uint64 indexed chainSelector, bytes extraArgs); + event SetRewardsExtraArgs(uint64 indexed chainSelector, bytes extraArgs); event SetWrappedRewardToken(address indexed token, address rewardToken); /** @@ -44,9 +46,8 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { /** * @notice Claims and distributes rewards between all secondary chains - * @param _extraArgs list of extra args as defined in CCIP API to be used for distribution to each chain **/ - function distributeRewards(bytes[] memory _extraArgs) external { + function distributeRewards() external { uint256 totalRESDL = ISDLPoolPrimary(sdlPool).effectiveBalanceOf(address(this)); address[] memory tokens = ISDLPoolPrimary(sdlPool).supportedTokens(); uint256 numDestinations = whitelistedChains.length; @@ -81,7 +82,7 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { } for (uint256 i = 0; i < numDestinations; ++i) { - _distributeRewards(whitelistedChains[i], _extraArgs[i], tokens, distributionAmounts[i]); + _distributeRewards(whitelistedChains[i], tokens, distributionAmounts[i]); } } @@ -159,19 +160,22 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { * @notice Whitelists a new chain * @param _chainSelector id of chain * @param _destination address to receive CCIP messages on chain - * @param _extraArgs extraArgs for this destination as defined in CCIP docs + * @param _updateExtraArgs extraArgs for sending updates to this destination as defined in CCIP docs + * @param _rewardsExtraArgs extraArgs for sending rewards to this destination as defined in CCIP docs **/ function addWhitelistedChain( uint64 _chainSelector, address _destination, - bytes calldata _extraArgs + bytes calldata _updateExtraArgs, + bytes calldata _rewardsExtraArgs ) external onlyOwner { if (whitelistedDestinations[_chainSelector] != address(0)) revert AlreadyAdded(); if (_destination == address(0)) revert InvalidDestination(); whitelistedChains.push(_chainSelector); whitelistedDestinations[_chainSelector] = _destination; - extraArgsByChain[_chainSelector] = _extraArgs; - emit ChainAdded(_chainSelector, _destination, _extraArgs); + updateExtraArgsByChain[_chainSelector] = _updateExtraArgs; + rewardsExtraArgsByChain[_chainSelector] = _rewardsExtraArgs; + emit ChainAdded(_chainSelector, _destination, _updateExtraArgs, _rewardsExtraArgs); } /** @@ -190,7 +194,8 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { } delete whitelistedDestinations[_chainSelector]; - delete extraArgsByChain[_chainSelector]; + delete updateExtraArgsByChain[_chainSelector]; + delete rewardsExtraArgsByChain[_chainSelector]; } /** @@ -215,26 +220,35 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { } /** - * @notice Sets the extra args for a chain + * @notice Sets the extra args used for sending updates to a chain * @param _chainSelector id of chain - * @param _extraArgs extra args as defined in CCIP API + * @param _updateExtraArgs extra args as defined in CCIP API **/ - function setExtraArgs(uint64 _chainSelector, bytes calldata _extraArgs) external onlyOwner { + function setUpdateExtraArgs(uint64 _chainSelector, bytes calldata _updateExtraArgs) external onlyOwner { if (whitelistedDestinations[_chainSelector] == address(0)) revert InvalidDestination(); - extraArgsByChain[_chainSelector] = _extraArgs; - emit SetExtraArgs(_chainSelector, _extraArgs); + updateExtraArgsByChain[_chainSelector] = _updateExtraArgs; + emit SetUpdateExtraArgs(_chainSelector, _updateExtraArgs); + } + + /** + * @notice Sets the extra args used for sending rewards to a chain + * @param _chainSelector id of chain + * @param _rewardsExtraArgs extra args as defined in CCIP API + **/ + function setRewardsExtraArgs(uint64 _chainSelector, bytes calldata _rewardsExtraArgs) external onlyOwner { + if (whitelistedDestinations[_chainSelector] == address(0)) revert InvalidDestination(); + rewardsExtraArgsByChain[_chainSelector] = _rewardsExtraArgs; + emit SetRewardsExtraArgs(_chainSelector, _rewardsExtraArgs); } /** * @notice Distributes rewards to a single chain * @param _destinationChainSelector id of chain - * @param _extraArgs extra args as defined in CCIP API * @param _rewardTokens list of reward tokens to distribute * @param _rewardTokenAmounts list of reward token amounts to distribute **/ function _distributeRewards( uint64 _destinationChainSelector, - bytes memory _extraArgs, address[] memory _rewardTokens, uint256[] memory _rewardTokenAmounts ) internal { @@ -266,7 +280,7 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { 0, rewardTokens, rewardTokenAmounts, - _extraArgs + rewardsExtraArgsByChain[_destinationChainSelector] ); IRouterClient router = IRouterClient(this.getRouter()); @@ -314,7 +328,7 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { _mintStartIndex, new address[](0), new uint256[](0), - extraArgsByChain[_destinationChainSelector] + updateExtraArgsByChain[_destinationChainSelector] ); IRouterClient router = IRouterClient(this.getRouter()); diff --git a/contracts/core/ccip/WrappedTokenBridge.sol b/contracts/core/ccip/WrappedTokenBridge.sol index 78a196c8..0a9d2587 100644 --- a/contracts/core/ccip/WrappedTokenBridge.sol +++ b/contracts/core/ccip/WrappedTokenBridge.sol @@ -76,7 +76,7 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { * @param _sender address of sender * @param _value amount of tokens transferred * @param _calldata encoded calldata consisting of destinationChainSelector (uint64), receiver (address), - * maxLINKFee (uint256), extraArgs (bytes) + * maxLINKFee (uint256) **/ function onTokenTransfer( address _sender, @@ -86,11 +86,11 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { if (msg.sender != address(token)) revert InvalidSender(); if (_value == 0) revert InvalidValue(); - (uint64 destinationChainSelector, address receiver, uint256 maxLINKFee, bytes memory extraArgs) = abi.decode( + (uint64 destinationChainSelector, address receiver, uint256 maxLINKFee) = abi.decode( _calldata, - (uint64, address, uint256, bytes) + (uint64, address, uint256) ); - _transferTokens(destinationChainSelector, _sender, receiver, _value, false, maxLINKFee, extraArgs); + _transferTokens(destinationChainSelector, _sender, receiver, _value, false, maxLINKFee); } /** @@ -100,38 +100,29 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { * @param _amount amount of tokens to transfer * @param _payNative whether fee should be paid natively or with LINK * @param _maxLINKFee call will revert if LINK fee exceeds this value - * @param _extraArgs encoded args as defined in CCIP API **/ function transferTokens( uint64 _destinationChainSelector, address _receiver, uint256 _amount, bool _payNative, - uint256 _maxLINKFee, - bytes memory _extraArgs + uint256 _maxLINKFee ) external payable onlyOwner returns (bytes32 messageId) { token.safeTransferFrom(msg.sender, address(this), _amount); - return - _transferTokens(_destinationChainSelector, msg.sender, _receiver, _amount, _payNative, _maxLINKFee, _extraArgs); + return _transferTokens(_destinationChainSelector, msg.sender, _receiver, _amount, _payNative, _maxLINKFee); } /** * @notice Returns the current fee for a token transfer * @param _destinationChainSelector id of destination chain * @param _payNative whether fee should be paid natively or with LINK - * @param _extraArgs encoded args as defined in CCIP API * @return fee current fee **/ - function getFee( - uint64 _destinationChainSelector, - bool _payNative, - bytes memory _extraArgs - ) external view returns (uint256) { + function getFee(uint64 _destinationChainSelector, bool _payNative) external view returns (uint256) { Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( address(this), 1000 ether, - _payNative ? address(0) : address(linkToken), - _extraArgs + _payNative ? address(0) : address(linkToken) ); return IRouterClient(this.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); @@ -157,7 +148,6 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { * @param _amount amount of tokens to transfer * @param _payNative whether fee should be paid natively or with LINK * @param _maxLINKFee call will revert if LINK fee exceeds this value - * @param _extraArgs encoded args as defined in CCIP API **/ function _transferTokens( uint64 _destinationChainSelector, @@ -165,8 +155,7 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { address _receiver, uint256 _amount, bool _payNative, - uint256 _maxLINKFee, - bytes memory _extraArgs + uint256 _maxLINKFee ) internal returns (bytes32 messageId) { uint256 preWrapBalance = wrappedToken.balanceOf(address(this)); wrappedToken.wrap(_amount); @@ -175,8 +164,7 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( _receiver, amountToTransfer, - _payNative ? address(0) : address(linkToken), - _extraArgs + _payNative ? address(0) : address(linkToken) ); IRouterClient router = IRouterClient(this.getRouter()); @@ -212,13 +200,11 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { * @param _receiver address to receive tokens on destination chain * @param _amount amount of tokens to transfer * @param _feeTokenAddress address of token that fees will be paid in - * @param _extraArgs encoded args as defined in CCIP API **/ function _buildCCIPMessage( address _receiver, uint256 _amount, - address _feeTokenAddress, - bytes memory _extraArgs + address _feeTokenAddress ) internal view returns (Client.EVM2AnyMessage memory) { Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({token: address(wrappedToken), amount: _amount}); @@ -228,7 +214,7 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { receiver: abi.encode(_receiver), data: "", tokenAmounts: tokenAmounts, - extraArgs: _extraArgs, + extraArgs: "0x", feeToken: _feeTokenAddress }); From a05a849979df416d4ff07f87d2c852a033e00077 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Thu, 7 Dec 2023 12:14:26 -0500 Subject: [PATCH 23/42] misc sdl pool ccip fixes --- contracts/core/ccip/RESDLTokenBridge.sol | 3 +- .../ccip/SDLPoolCCIPControllerSecondary.sol | 28 ++++--------------- contracts/core/ccip/WrappedTokenBridge.sol | 6 ++++ .../core/ccip/base/SDLPoolCCIPController.sol | 3 ++ contracts/core/sdlPool/SDLPoolSecondary.sol | 9 +++++- contracts/core/sdlPool/base/SDLPool.sol | 2 ++ 6 files changed, 27 insertions(+), 24 deletions(-) diff --git a/contracts/core/ccip/RESDLTokenBridge.sol b/contracts/core/ccip/RESDLTokenBridge.sol index 5280f47b..24738eea 100644 --- a/contracts/core/ccip/RESDLTokenBridge.sol +++ b/contracts/core/ccip/RESDLTokenBridge.sol @@ -63,6 +63,7 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { error InvalidReceiver(); error AlreadyAdded(); error AlreadyRemoved(); + error InvalidMsgValue(); /** * @notice Initializes the contract @@ -109,8 +110,8 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { address sender = msg.sender; if (sender != sdlPool.ownerOf(_tokenId)) revert SenderNotAuthorized(); if (_receiver == address(0)) revert InvalidReceiver(); - if (whitelistedDestinations[_destinationChainSelector] == address(0)) revert InvalidDestination(); + if (_payNative == false && msg.value != 0) revert InvalidMsgValue(); RESDLToken memory reSDLToken; { diff --git a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol index 92789172..798beef8 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol @@ -15,13 +15,12 @@ interface ISDLPoolSecondary is ISDLPool { contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { using SafeERC20 for IERC20; - uint64 public timeOfLastUpdate; - uint64 public timeBetweenUpdates; - uint64 public immutable primaryChainSelector; address public immutable primaryChainDestination; bytes public extraArgs; + bool public shouldUpdate; + event SetExtraArgs(bytes extraArgs); error UpdateConditionsNotMet(); @@ -35,7 +34,6 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { * @param _primaryChainSelector id of the primary chain * @param _primaryChainDestination address to receive messages on primary chain * @param _maxLINKFee max fee to be paid on an outgoing message - * @param _timeBetweenUpdates min amount of time (seconds) between updates * @param _extraArgs extra args as defined in CCIP API to be used for outgoing messages **/ constructor( @@ -46,12 +44,10 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { uint64 _primaryChainSelector, address _primaryChainDestination, uint256 _maxLINKFee, - uint64 _timeBetweenUpdates, bytes memory _extraArgs ) SDLPoolCCIPController(_router, _linkToken, _sdlToken, _sdlPool, _maxLINKFee) { primaryChainSelector = _primaryChainSelector; primaryChainDestination = _primaryChainDestination; - timeBetweenUpdates = _timeBetweenUpdates; extraArgs = _extraArgs; } @@ -61,11 +57,7 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { * @return whether an update should be initiated **/ function checkUpkeep(bytes calldata) external view returns (bool, bytes memory) { - if (ISDLPoolSecondary(sdlPool).shouldUpdate() && block.timestamp > timeOfLastUpdate + timeBetweenUpdates) { - return (true, "0x"); - } - - return (false, "0x"); + return (shouldUpdate, "0x"); } /** @@ -73,10 +65,9 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { * @dev used by Chainlink automation **/ function performUpkeep(bytes calldata) external { - if (!ISDLPoolSecondary(sdlPool).shouldUpdate() || block.timestamp <= timeOfLastUpdate + timeBetweenUpdates) - revert UpdateConditionsNotMet(); + if (!shouldUpdate) revert UpdateConditionsNotMet(); - timeOfLastUpdate = uint64(block.timestamp); + shouldUpdate = false; _initiateUpdate(primaryChainSelector, primaryChainDestination, extraArgs); } @@ -136,14 +127,6 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { ); } - /** - * @notice Sets the min amount of time between updates - * @param _timeBetweenUpdates min amount of time (seconds) - **/ - function setTimeBetweenUpdates(uint64 _timeBetweenUpdates) external onlyOwner { - timeBetweenUpdates = _timeBetweenUpdates; - } - /** * @notice Sets the extra args for sending updates to the primary chain * @param _extraArgs extra args as defined in CCIP API @@ -201,6 +184,7 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { IERC20(rewardTokens[i]).safeTransfer(sdlPool, _any2EvmMessage.destTokenAmounts[i].amount); } ISDLPoolSecondary(sdlPool).distributeTokens(rewardTokens); + if (ISDLPoolSecondary(sdlPool).shouldUpdate()) shouldUpdate = true; } } else { uint256 mintStartIndex = abi.decode(_any2EvmMessage.data, (uint256)); diff --git a/contracts/core/ccip/WrappedTokenBridge.sol b/contracts/core/ccip/WrappedTokenBridge.sol index 0a9d2587..e79ef199 100644 --- a/contracts/core/ccip/WrappedTokenBridge.sol +++ b/contracts/core/ccip/WrappedTokenBridge.sol @@ -47,6 +47,8 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { error TransferFailed(); error FeeExceedsLimit(); error InvalidMessage(); + error InvalidMsgValue(); + error InvalidReceiver(); /** * @notice Initializes the contract @@ -108,6 +110,8 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { bool _payNative, uint256 _maxLINKFee ) external payable onlyOwner returns (bytes32 messageId) { + if (_payNative == false && msg.value != 0) revert InvalidMsgValue(); + token.safeTransferFrom(msg.sender, address(this), _amount); return _transferTokens(_destinationChainSelector, msg.sender, _receiver, _amount, _payNative, _maxLINKFee); } @@ -134,6 +138,8 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { * @param _receiver address to receive recovered tokens **/ function recoverTokens(address[] calldata _tokens, address _receiver) external onlyOwner { + if (_receiver == address(0)) revert InvalidReceiver(); + for (uint256 i = 0; i < _tokens.length; ++i) { IERC20 tokenToTransfer = IERC20(_tokens[i]); tokenToTransfer.safeTransfer(_receiver, tokenToTransfer.balanceOf(address(this))); diff --git a/contracts/core/ccip/base/SDLPoolCCIPController.sol b/contracts/core/ccip/base/SDLPoolCCIPController.sol index 985408f3..ed19d524 100644 --- a/contracts/core/ccip/base/SDLPoolCCIPController.sol +++ b/contracts/core/ccip/base/SDLPoolCCIPController.sol @@ -26,6 +26,7 @@ abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { error InvalidDestination(); error SenderNotAuthorized(); error FeeExceedsLimit(uint256 fee); + error InvalidReceiver(); modifier onlyBridge() { if (msg.sender != reSDLTokenBridge) revert OnlyRESDLTokenBridge(); @@ -60,6 +61,8 @@ abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { * @param _receiver address to receive recovered tokens **/ function recoverTokens(address[] calldata _tokens, address _receiver) external onlyOwner { + if (_receiver == address(0)) revert InvalidReceiver(); + for (uint256 i = 0; i < _tokens.length; ++i) { IERC20 tokenToTransfer = IERC20(_tokens[i]); tokenToTransfer.safeTransfer(_receiver, tokenToTransfer.balanceOf(address(this))); diff --git a/contracts/core/sdlPool/SDLPoolSecondary.sol b/contracts/core/sdlPool/SDLPoolSecondary.sol index 170de200..ce79fbbb 100644 --- a/contracts/core/sdlPool/SDLPoolSecondary.sol +++ b/contracts/core/sdlPool/SDLPoolSecondary.sol @@ -22,6 +22,7 @@ contract SDLPoolSecondary is SDLPool { mapping(uint256 => LockUpdate[]) internal queuedLockUpdates; + uint256 public queuedNewLockLimit; uint256[] internal currentMintLockIdByBatch; Lock[][] internal queuedNewLocks; mapping(address => NewLockPointer[]) internal newLocksByOwner; @@ -47,6 +48,7 @@ contract SDLPoolSecondary is SDLPool { error CannotTransferWithQueuedUpdates(); error UpdateInProgress(); error NoUpdateInProgress(); + error TooManyQueuedLocks(); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -59,18 +61,21 @@ contract SDLPoolSecondary is SDLPool { * @param _symbol symbol of the staking derivative token * @param _sdlToken address of the SDL token * @param _boostController address of the boost controller + * @param _queuedNewLockLimit max amount of queued new locks an account can have **/ function initialize( string memory _name, string memory _symbol, address _sdlToken, - address _boostController + address _boostController, + uint256 _queuedNewLockLimit ) public initializer { __SDLPoolBase_init(_name, _symbol, _sdlToken, _boostController); updateBatchIndex = 1; currentMintLockIdByBatch.push(0); queuedNewLocks.push(); queuedNewLocks.push(); + queuedNewLockLimit = _queuedNewLockLimit; } /** @@ -368,6 +373,8 @@ contract SDLPoolSecondary is SDLPool { uint256 _amount, uint64 _lockingDuration ) internal { + if (newLocksByOwner[_owner].length >= queuedNewLockLimit) revert TooManyQueuedLocks(); + Lock memory lock = _createLock(_amount, _lockingDuration); queuedNewLocks[updateBatchIndex].push(lock); newLocksByOwner[_owner].push(NewLockPointer(updateBatchIndex, uint128(queuedNewLocks[updateBatchIndex].length - 1))); diff --git a/contracts/core/sdlPool/base/SDLPool.sol b/contracts/core/sdlPool/base/SDLPool.sol index 4912667c..2409607c 100644 --- a/contracts/core/sdlPool/base/SDLPool.sol +++ b/contracts/core/sdlPool/base/SDLPool.sol @@ -69,6 +69,7 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp error TransferFromIncorrectOwner(); error TransferToZeroAddress(); error TransferToNonERC721Implementer(); + error TransferToCCIPController(); error ApprovalToCurrentOwner(); error ApprovalToCaller(); error OnlyCCIPController(); @@ -460,6 +461,7 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp ) internal virtual { if (_from != ownerOf(_lockId)) revert TransferFromIncorrectOwner(); if (_to == address(0)) revert TransferToZeroAddress(); + if (_to == ccipController) revert TransferToCCIPController(); delete tokenApprovals[_lockId]; From 11c7bf25e189828d8560ece50509bab2f72050f3 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Fri, 8 Dec 2023 09:39:03 -0500 Subject: [PATCH 24/42] added rewards initiator for fair rewards distribution --- ...lashingKeeper.sol => RewardsInitiator.sol} | 42 +++-- contracts/core/StakingPool.sol | 149 ++++++++++-------- .../ccip/SDLPoolCCIPControllerPrimary.sol | 18 ++- .../ISDLPoolCCIPControllerPrimary.sol | 8 + 4 files changed, 141 insertions(+), 76 deletions(-) rename contracts/core/{SlashingKeeper.sol => RewardsInitiator.sol} (51%) create mode 100644 contracts/core/interfaces/ISDLPoolCCIPControllerPrimary.sol diff --git a/contracts/core/SlashingKeeper.sol b/contracts/core/RewardsInitiator.sol similarity index 51% rename from contracts/core/SlashingKeeper.sol rename to contracts/core/RewardsInitiator.sol index b00e0138..9896a55f 100644 --- a/contracts/core/SlashingKeeper.sol +++ b/contracts/core/RewardsInitiator.sol @@ -3,20 +3,38 @@ pragma solidity 0.8.15; import "./interfaces/IStakingPool.sol"; import "./interfaces/IStrategy.sol"; +import "./interfaces/ISDLPoolCCIPControllerPrimary.sol"; /** - * @title Slashing Keeper - * @notice Updates strategy rewards if any losses have been incurred + * @title Rewards Initiator + * @notice Updates and distributes rewards across the staking pool and cross-chain SDL Pools + * @dev Chainlink automation should call updateRewards periodically under normal circumstances and call performUpkeep + * in the case of a negative rebase in the staking pool */ contract SlashingKeeper { IStakingPool public stakingPool; + ISDLPoolCCIPControllerPrimary public sdlPoolCCIPController; - constructor(address _stakingPool) { + error NoStrategiesToUpdate(); + error PositiveDepositChange(); + + constructor(address _stakingPool, address _sdlPoolCCIPController) { stakingPool = IStakingPool(_stakingPool); + sdlPoolCCIPController = ISDLPoolCCIPControllerPrimary(_sdlPoolCCIPController); } /** - * @notice returns whether or not rewards should be updated and the strategies to update + * @notice updates strategy rewards in the staking pool and distributes rewards to cross-chain SDL pools + * @param _strategyIdxs indexes of strategies to update rewards for + * @param _data encoded data to be passed to each strategy + **/ + function updateRewards(uint256[] calldata _strategyIdxs, bytes calldata _data) external { + stakingPool.updateStrategyRewards(_strategyIdxs, _data); + sdlPoolCCIPController.distributeRewards(); + } + + /** + * @notice returns whether or not rewards should be updated due to a neagtive rebase and the strategies to update * @return upkeepNeeded whether or not rewards should be updated * @return performData abi encoded list of strategy indexes to update **/ @@ -25,7 +43,7 @@ contract SlashingKeeper { bool[] memory strategiesToUpdate = new bool[](strategies.length); uint256 totalStrategiesToUpdate; - for (uint256 i = 0; i < strategies.length; i++) { + for (uint256 i = 0; i < strategies.length; ++i) { IStrategy strategy = IStrategy(strategies[i]); if (strategy.getDepositChange() < 0) { strategiesToUpdate[i] = true; @@ -33,11 +51,11 @@ contract SlashingKeeper { } } - if (totalStrategiesToUpdate > 0) { + if (totalStrategiesToUpdate != 0) { uint256[] memory strategyIdxs = new uint256[](totalStrategiesToUpdate); uint256 strategiesAdded; - for (uint256 i = 0; i < strategiesToUpdate.length; i++) { + for (uint256 i = 0; i < strategiesToUpdate.length; ++i) { if (strategiesToUpdate[i]) { strategyIdxs[strategiesAdded] = i; strategiesAdded++; @@ -51,17 +69,19 @@ contract SlashingKeeper { } /** - * @notice Updates rewards + * @notice Updates rewards in the case of a negative rebase * @param _performData abi encoded list of strategy indexes to update */ function performUpkeep(bytes calldata _performData) external { address[] memory strategies = stakingPool.getStrategies(); uint256[] memory strategiesToUpdate = abi.decode(_performData, (uint256[])); - require(strategiesToUpdate.length > 0, "No strategies to update"); - for (uint256 i = 0; i < strategiesToUpdate.length; i++) { - require(IStrategy(strategies[strategiesToUpdate[i]]).getDepositChange() < 0, "Deposit change is >= 0"); + if (strategiesToUpdate.length == 0) revert NoStrategiesToUpdate(); + + for (uint256 i = 0; i < strategiesToUpdate.length; ++i) { + if (IStrategy(strategies[strategiesToUpdate[i]]).getDepositChange() >= 0) revert PositiveDepositChange(); } + stakingPool.updateStrategyRewards(strategiesToUpdate, ""); } } diff --git a/contracts/core/StakingPool.sol b/contracts/core/StakingPool.sol index 4e0a7b1b..bd5273ca 100644 --- a/contracts/core/StakingPool.sol +++ b/contracts/core/StakingPool.sol @@ -26,11 +26,13 @@ contract StakingPool is StakingRewardsPool { Fee[] private fees; address public priorityPool; - address private delegatorPool; // deprecated + address public rewardsInitiator; uint16 private poolIndex; // deprecated event UpdateStrategyRewards(address indexed account, uint256 totalStaked, int rewardsAmount, uint256 totalFees); + error SenderNotAuthorized(); + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -50,7 +52,7 @@ contract StakingPool is StakingRewardsPool { } modifier onlyPriorityPool() { - require(priorityPool == msg.sender, "PriorityPool only"); + if (msg.sender != priorityPool) revert SenderNotAuthorized(); _; } @@ -239,7 +241,7 @@ contract StakingPool is StakingRewardsPool { uint256[] memory idxs = new uint256[](1); idxs[0] = _index; - updateStrategyRewards(idxs, _strategyUpdateData); + _updateStrategyRewards(idxs, _strategyUpdateData); IStrategy strategy = IStrategy(strategies[_index]); uint256 totalStrategyDeposits = strategy.getTotalDeposits(); @@ -342,7 +344,86 @@ contract StakingPool is StakingRewardsPool { * @param _strategyIdxs indexes of strategies to update rewards for * @param _data encoded data to be passed to each strategy **/ - function updateStrategyRewards(uint256[] memory _strategyIdxs, bytes memory _data) public { + function updateStrategyRewards(uint256[] memory _strategyIdxs, bytes memory _data) external { + if (msg.sender != rewardsInitiator && !_strategyExists(msg.sender)) revert SenderNotAuthorized(); + _updateStrategyRewards(_strategyIdxs, _data); + } + + /** + * @notice deposits available liquidity into strategies by order of priority + * @dev deposits into strategies[0] until its limit is reached, then strategies[1], and so on + **/ + function depositLiquidity() public { + uint256 toDeposit = token.balanceOf(address(this)); + if (toDeposit > 0) { + for (uint256 i = 0; i < strategies.length; i++) { + IStrategy strategy = IStrategy(strategies[i]); + uint256 strategyCanDeposit = strategy.canDeposit(); + if (strategyCanDeposit >= toDeposit) { + strategy.deposit(toDeposit); + break; + } else if (strategyCanDeposit > 0) { + strategy.deposit(strategyCanDeposit); + toDeposit -= strategyCanDeposit; + } + } + } + } + + /** + * @notice Sets the priority pool + * @param _priorityPool address of priority pool + **/ + function setPriorityPool(address _priorityPool) external onlyOwner { + priorityPool = _priorityPool; + } + + /** + * @notice Sets the rewards initiator + * @dev this address has sole authority to update rewards + * @param _rewardsInitiator address of rewards initiator + **/ + function setRewardsInitiator(address _rewardsInitiator) external onlyOwner { + rewardsInitiator = _rewardsInitiator; + } + + /** + * @notice returns the total amount of assets staked in the pool + * @return the total staked amount + */ + function _totalStaked() internal view override returns (uint256) { + return totalStaked; + } + + /** + * @notice withdraws liquidity from strategies in opposite order of priority + * @dev withdraws from strategies[strategies.length - 1], then strategies[strategies.length - 2], and so on + * until withdraw amount is reached + * @param _amount amount to withdraw + **/ + function _withdrawLiquidity(uint256 _amount) private { + uint256 toWithdraw = _amount; + + for (uint256 i = strategies.length; i > 0; i--) { + IStrategy strategy = IStrategy(strategies[i - 1]); + uint256 strategyCanWithdrawdraw = strategy.canWithdraw(); + + if (strategyCanWithdrawdraw >= toWithdraw) { + strategy.withdraw(toWithdraw); + break; + } else if (strategyCanWithdrawdraw > 0) { + strategy.withdraw(strategyCanWithdrawdraw); + toWithdraw -= strategyCanWithdrawdraw; + } + } + } + + /** + * @notice updates and distributes rewards based on balance changes in strategies + * @param _strategyIdxs indexes of strategies to update rewards for + * @param _data encoded data to be passed to each strategy + **/ + function _updateStrategyRewards(uint256[] memory _strategyIdxs, bytes memory _data) private { int256 totalRewards; uint256 totalFeeAmounts; uint256 totalFeeCount; @@ -406,66 +487,6 @@ contract StakingPool is StakingRewardsPool { emit UpdateStrategyRewards(msg.sender, totalStaked, totalRewards, totalFeeAmounts); } - /** - * @notice deposits available liquidity into strategies by order of priority - * @dev deposits into strategies[0] until its limit is reached, then strategies[1], and so on - **/ - function depositLiquidity() public { - uint256 toDeposit = token.balanceOf(address(this)); - if (toDeposit > 0) { - for (uint256 i = 0; i < strategies.length; i++) { - IStrategy strategy = IStrategy(strategies[i]); - uint256 strategyCanDeposit = strategy.canDeposit(); - if (strategyCanDeposit >= toDeposit) { - strategy.deposit(toDeposit); - break; - } else if (strategyCanDeposit > 0) { - strategy.deposit(strategyCanDeposit); - toDeposit -= strategyCanDeposit; - } - } - } - } - - /** - * @notice Sets the priority pool - * @param _priorityPool address of priority pool - **/ - function setPriorityPool(address _priorityPool) external onlyOwner { - priorityPool = _priorityPool; - } - - /** - * @notice returns the total amount of assets staked in the pool - * @return the total staked amount - */ - function _totalStaked() internal view override returns (uint256) { - return totalStaked; - } - - /** - * @notice withdraws liquidity from strategies in opposite order of priority - * @dev withdraws from strategies[strategies.length - 1], then strategies[strategies.length - 2], and so on - * until withdraw amount is reached - * @param _amount amount to withdraw - **/ - function _withdrawLiquidity(uint256 _amount) private { - uint256 toWithdraw = _amount; - - for (uint256 i = strategies.length; i > 0; i--) { - IStrategy strategy = IStrategy(strategies[i - 1]); - uint256 strategyCanWithdrawdraw = strategy.canWithdraw(); - - if (strategyCanWithdrawdraw >= toWithdraw) { - strategy.withdraw(toWithdraw); - break; - } else if (strategyCanWithdrawdraw > 0) { - strategy.withdraw(strategyCanWithdrawdraw); - toWithdraw -= strategyCanWithdrawdraw; - } - } - } - /** * @notice returns the sum of all fees * @return sum of fees in basis points diff --git a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol index d78b46e3..c5490c29 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol @@ -21,6 +21,8 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { mapping(address => address) public wrappedRewardTokens; + address public rewardsInitiator; + event DistributeRewards(bytes32 indexed messageId, uint64 indexed destinationChainSelector, uint256 fees); event ChainAdded(uint64 indexed chainSelector, address destination, bytes updateExtraArgs, bytes rewardsExtraArgs); event ChainRemoved(uint64 indexed destinationChainSelector, address destination); @@ -44,10 +46,15 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { uint256 _maxLINKFee ) SDLPoolCCIPController(_router, _linkToken, _sdlToken, _sdlPool, _maxLINKFee) {} + modifier onlyRewardsInitiator() { + if (msg.sender != rewardsInitiator) revert SenderNotAuthorized(); + _; + } + /** * @notice Claims and distributes rewards between all secondary chains **/ - function distributeRewards() external { + function distributeRewards() external onlyRewardsInitiator { uint256 totalRESDL = ISDLPoolPrimary(sdlPool).effectiveBalanceOf(address(this)); address[] memory tokens = ISDLPoolPrimary(sdlPool).supportedTokens(); uint256 numDestinations = whitelistedChains.length; @@ -241,6 +248,15 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { emit SetRewardsExtraArgs(_chainSelector, _rewardsExtraArgs); } + /** + * @notice Sets the rewards initiator + * @dev this address has sole authority to update rewards + * @param _rewardsInitiator address of rewards initiator + **/ + function setRewardsInitiator(address _rewardsInitiator) external onlyOwner { + rewardsInitiator = _rewardsInitiator; + } + /** * @notice Distributes rewards to a single chain * @param _destinationChainSelector id of chain diff --git a/contracts/core/interfaces/ISDLPoolCCIPControllerPrimary.sol b/contracts/core/interfaces/ISDLPoolCCIPControllerPrimary.sol new file mode 100644 index 00000000..f33bb98a --- /dev/null +++ b/contracts/core/interfaces/ISDLPoolCCIPControllerPrimary.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.15; + +import "./ISDLPoolCCIPController.sol"; + +interface ISDLPoolCCIPControllerPrimary is ISDLPoolCCIPController { + function distributeRewards() external; +} From 0086bccc4d6adb51ddf35e07d505414c39c69c30 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Tue, 12 Dec 2023 08:51:34 -0500 Subject: [PATCH 25/42] use shared ccip client for sdl pool messages --- contracts/core/ccip/RESDLTokenBridge.sol | 162 ++++++------------ .../ccip/SDLPoolCCIPControllerPrimary.sol | 67 +++----- .../ccip/SDLPoolCCIPControllerSecondary.sol | 65 +++---- contracts/core/ccip/WrappedTokenBridge.sol | 18 +- .../core/ccip/base/SDLPoolCCIPController.sol | 63 ++++++- .../core/interfaces/IRESDLTokenBridge.sol | 8 + contracts/core/interfaces/ISDLPool.sol | 28 ++- .../interfaces/ISDLPoolCCIPController.sol | 30 ++-- contracts/core/sdlPool/SDLPoolPrimary.sol | 16 +- contracts/core/sdlPool/SDLPoolSecondary.sol | 16 +- .../core/test/SDLPoolCCIPControllerMock.sol | 24 +-- 11 files changed, 213 insertions(+), 284 deletions(-) create mode 100644 contracts/core/interfaces/IRESDLTokenBridge.sol diff --git a/contracts/core/ccip/RESDLTokenBridge.sol b/contracts/core/ccip/RESDLTokenBridge.sol index 24738eea..e95e8dd3 100644 --- a/contracts/core/ccip/RESDLTokenBridge.sol +++ b/contracts/core/ccip/RESDLTokenBridge.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.15; import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol"; import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; -import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -14,26 +13,16 @@ import "../interfaces/ISDLPoolCCIPController.sol"; * @title reSDL Token Bridge * @notice Handles CCIP transfers of reSDL NFTs */ -contract RESDLTokenBridge is Ownable, CCIPReceiver { +contract RESDLTokenBridge is Ownable { using SafeERC20 for IERC20; - struct RESDLToken { - uint256 amount; - uint256 boostAmount; - uint64 startTime; - uint64 duration; - uint64 expiry; - } - IERC20 public linkToken; IERC20 public sdlToken; ISDLPool public sdlPool; ISDLPoolCCIPController public sdlPoolCCIPController; - mapping(uint64 => address) public whitelistedDestinations; - - bytes public extraArgs; + mapping(uint64 => bytes) public extraArgsByChain; event TokenTransferred( bytes32 indexed messageId, @@ -51,45 +40,37 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { address receiver, uint256 tokenId ); - event DestinationAdded(uint64 indexed destinationChainSelector, address destination); - event DestinationRemoved(uint64 indexed destinationChainSelector, address destination); - event SetExtraArgs(bytes extraArgs); + event SetExtraArgs(uint64 indexed chainSelector, bytes extraArgs); error InsufficientFee(); error TransferFailed(); error FeeExceedsLimit(); error SenderNotAuthorized(); - error InvalidDestination(); error InvalidReceiver(); - error AlreadyAdded(); - error AlreadyRemoved(); error InvalidMsgValue(); /** * @notice Initializes the contract - * @param _router address of the CCIP router * @param _linkToken address of the LINK token * @param _sdlToken address of the SDL token * @param _sdlPool address of the SDL Pool * @param _sdlPoolCCIPController address of the SDL Pool CCIP controller - * @param _extraArgs encoded args as defined in CCIP API used for sending transfers **/ constructor( - address _router, address _linkToken, address _sdlToken, address _sdlPool, - address _sdlPoolCCIPController, - bytes memory _extraArgs - ) CCIPReceiver(_router) { + address _sdlPoolCCIPController + ) { linkToken = IERC20(_linkToken); sdlToken = IERC20(_sdlToken); sdlPool = ISDLPool(_sdlPool); sdlPoolCCIPController = ISDLPoolCCIPController(_sdlPoolCCIPController); - extraArgs = _extraArgs; - linkToken.safeApprove(_router, type(uint256).max); - sdlToken.safeApprove(_router, type(uint256).max); - sdlToken.safeApprove(_sdlPoolCCIPController, type(uint256).max); + } + + modifier onlySDLPoolCCIPController() { + if (msg.sender != address(sdlPoolCCIPController)) revert SenderNotAuthorized(); + _; } /** @@ -107,48 +88,45 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { bool _payNative, uint256 _maxLINKFee ) external payable returns (bytes32 messageId) { - address sender = msg.sender; - if (sender != sdlPool.ownerOf(_tokenId)) revert SenderNotAuthorized(); + if (msg.sender != sdlPool.ownerOf(_tokenId)) revert SenderNotAuthorized(); if (_receiver == address(0)) revert InvalidReceiver(); - if (whitelistedDestinations[_destinationChainSelector] == address(0)) revert InvalidDestination(); if (_payNative == false && msg.value != 0) revert InvalidMsgValue(); - RESDLToken memory reSDLToken; - { - (uint256 amount, uint256 boostAmount, uint64 startTime, uint64 duration, uint64 expiry) = sdlPoolCCIPController - .handleOutgoingRESDL(_destinationChainSelector, sender, _tokenId); - reSDLToken = RESDLToken(amount, boostAmount, startTime, duration, expiry); - } + (address destination, ISDLPool.RESDLToken memory reSDLToken) = sdlPoolCCIPController.handleOutgoingRESDL( + _destinationChainSelector, + msg.sender, + _tokenId + ); + bytes memory extraArgs = extraArgsByChain[_destinationChainSelector]; Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( _receiver, _tokenId, reSDLToken, - whitelistedDestinations[_destinationChainSelector], + destination, _payNative ? address(0) : address(linkToken), extraArgs ); - IRouterClient router = IRouterClient(this.getRouter()); - uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage); + uint256 fees = IRouterClient(sdlPoolCCIPController.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); if (_payNative) { if (fees > msg.value) revert InsufficientFee(); - messageId = router.ccipSend{value: fees}(_destinationChainSelector, evm2AnyMessage); + messageId = sdlPoolCCIPController.ccipSend{value: fees}(_destinationChainSelector, evm2AnyMessage); if (fees < msg.value) { - (bool success, ) = sender.call{value: msg.value - fees}(""); + (bool success, ) = msg.sender.call{value: msg.value - fees}(""); if (!success) revert TransferFailed(); } } else { if (fees > _maxLINKFee) revert FeeExceedsLimit(); - linkToken.safeTransferFrom(sender, address(this), fees); - messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage); + linkToken.safeTransferFrom(msg.sender, address(sdlPoolCCIPController), fees); + messageId = sdlPoolCCIPController.ccipSend(_destinationChainSelector, evm2AnyMessage); } emit TokenTransferred( messageId, _destinationChainSelector, - sender, + msg.sender, _receiver, _tokenId, _payNative ? address(0) : address(linkToken), @@ -166,44 +144,51 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( address(this), 0, - RESDLToken(0, 0, 0, 0, 0), + ISDLPool.RESDLToken(0, 0, 0, 0, 0), address(this), _payNative ? address(0) : address(linkToken), - extraArgs + extraArgsByChain[_destinationChainSelector] ); - return IRouterClient(this.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); + return IRouterClient(sdlPoolCCIPController.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); } /** - * @notice Whitelists a new destination chain - * @param _destinationChainSelector id of destination chain - * @param _destination address to receive CCIP messages on destination chain + * @notice Sets the extra args used for sending reSDL to a chain + * @param _chainSelector id of chain + * @param _extraArgs extra args as defined in CCIP API **/ - function addWhitelistedDestination(uint64 _destinationChainSelector, address _destination) external onlyOwner { - if (whitelistedDestinations[_destinationChainSelector] != address(0)) revert AlreadyAdded(); - if (_destination == address(0)) revert InvalidDestination(); - whitelistedDestinations[_destinationChainSelector] = _destination; - emit DestinationAdded(_destinationChainSelector, _destination); + function setExtraArgs(uint64 _chainSelector, bytes calldata _extraArgs) external onlyOwner { + extraArgsByChain[_chainSelector] = _extraArgs; + emit SetExtraArgs(_chainSelector, _extraArgs); } /** - * @notice Removes an existing destination chain - * @param _destinationChainSelector id of destination chain + * @notice Processes a received message + * @dev handles incoming reSDL transfers + * @param _message CCIP message **/ - function removeWhitelistedDestination(uint64 _destinationChainSelector) external onlyOwner { - if (whitelistedDestinations[_destinationChainSelector] == address(0)) revert AlreadyRemoved(); - emit DestinationRemoved(_destinationChainSelector, whitelistedDestinations[_destinationChainSelector]); - delete whitelistedDestinations[_destinationChainSelector]; - } + function ccipReceive(Client.Any2EVMMessage memory _message) external onlySDLPoolCCIPController { + address sender = abi.decode(_message.sender, (address)); - /** - * @notice sets extra args used for reSDL transfers - * @param _extraArgs encoded args as defined in CCIP API - */ - function setExtraArgs(bytes calldata _extraArgs) external onlyOwner { - extraArgs = _extraArgs; - emit SetExtraArgs(_extraArgs); + ( + address receiver, + uint256 tokenId, + uint256 amount, + uint256 boostAmount, + uint64 startTime, + uint64 duration, + uint64 expiry + ) = abi.decode(_message.data, (address, uint256, uint256, uint256, uint64, uint64, uint64)); + + sdlPoolCCIPController.handleIncomingRESDL( + _message.sourceChainSelector, + receiver, + tokenId, + ISDLPool.RESDLToken(amount, boostAmount, startTime, duration, expiry) + ); + + emit TokenReceived(_message.messageId, _message.sourceChainSelector, sender, receiver, tokenId); } /** @@ -219,7 +204,7 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { function _buildCCIPMessage( address _receiver, uint256 _tokenId, - RESDLToken memory _reSDLToken, + ISDLPool.RESDLToken memory _reSDLToken, address _destination, address _feeTokenAddress, bytes memory _extraArgs @@ -249,37 +234,4 @@ contract RESDLTokenBridge is Ownable, CCIPReceiver { return evm2AnyMessage; } - - /** - * @notice Processes a received message - * @dev handles incoming reSDL transfers - * @param _any2EvmMessage CCIP message - **/ - function _ccipReceive(Client.Any2EVMMessage memory _any2EvmMessage) internal override { - address sender = abi.decode(_any2EvmMessage.sender, (address)); - if (sender != whitelistedDestinations[_any2EvmMessage.sourceChainSelector]) revert SenderNotAuthorized(); - - ( - address receiver, - uint256 tokenId, - uint256 amount, - uint256 boostAmount, - uint64 startTime, - uint64 duration, - uint64 expiry - ) = abi.decode(_any2EvmMessage.data, (address, uint256, uint256, uint256, uint64, uint64, uint64)); - - sdlPoolCCIPController.handleIncomingRESDL( - _any2EvmMessage.sourceChainSelector, - receiver, - tokenId, - amount, - boostAmount, - startTime, - duration, - expiry - ); - - emit TokenReceived(_any2EvmMessage.messageId, _any2EvmMessage.sourceChainSelector, sender, receiver, tokenId); - } } diff --git a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol index c5490c29..326880e9 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.15; import "./base/SDLPoolCCIPController.sol"; -import "../interfaces/ISDLPool.sol"; import "../interfaces/IERC677.sol"; interface ISDLPoolPrimary is ISDLPool { @@ -25,7 +24,7 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { event DistributeRewards(bytes32 indexed messageId, uint64 indexed destinationChainSelector, uint256 fees); event ChainAdded(uint64 indexed chainSelector, address destination, bytes updateExtraArgs, bytes rewardsExtraArgs); - event ChainRemoved(uint64 indexed destinationChainSelector, address destination); + event ChainRemoved(uint64 indexed chainSelector, address destination); event SetUpdateExtraArgs(uint64 indexed chainSelector, bytes extraArgs); event SetRewardsExtraArgs(uint64 indexed chainSelector, bytes extraArgs); event SetWrappedRewardToken(address indexed token, address rewardToken); @@ -98,27 +97,21 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { * @param _destinationChainSelector id of the destination chain * @param _sender sender of the transfer * @param _tokenId id of token + * @return the destination address * @return the token being transferred **/ function handleOutgoingRESDL( uint64 _destinationChainSelector, address _sender, uint256 _tokenId - ) - external - onlyBridge - returns ( - uint256, - uint256, - uint64, - uint64, - uint64 - ) - { - (uint256 amount, uint256 boostAmount, uint64 startTime, uint64 duration, uint64 expiry) = ISDLPoolPrimary(sdlPool) - .handleOutgoingRESDL(_sender, _tokenId, reSDLTokenBridge); - reSDLSupplyByChain[_destinationChainSelector] += amount + boostAmount; - return (amount, boostAmount, startTime, duration, expiry); + ) external override onlyBridge returns (address, ISDLPool.RESDLToken memory) { + ISDLPool.RESDLToken memory reSDLToken = ISDLPoolPrimary(sdlPool).handleOutgoingRESDL( + _sender, + _tokenId, + reSDLTokenBridge + ); + reSDLSupplyByChain[_destinationChainSelector] += reSDLToken.amount + reSDLToken.boostAmount; + return (whitelistedDestinations[_destinationChainSelector], reSDLToken); } /** @@ -126,33 +119,17 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { * @param _sourceChainSelector id of the source chain * @param _receiver receiver of the transfer * @param _tokenId id of reSDL token - * @param _amount amount of underlying SDL - * @param _boostAmount reSDL boost amount - * @param _startTime start time of the lock - * @param _duration duration of the lock - * @param _expiry expiry time of the lock + * @param _reSDLToken reSDL token **/ function handleIncomingRESDL( uint64 _sourceChainSelector, address _receiver, uint256 _tokenId, - uint256 _amount, - uint256 _boostAmount, - uint64 _startTime, - uint64 _duration, - uint64 _expiry - ) external onlyBridge { - sdlToken.safeTransferFrom(reSDLTokenBridge, sdlPool, _amount); - ISDLPoolPrimary(sdlPool).handleIncomingRESDL( - _receiver, - _tokenId, - _amount, - _boostAmount, - _startTime, - _duration, - _expiry - ); - reSDLSupplyByChain[_sourceChainSelector] -= _amount + _boostAmount; + ISDLPool.RESDLToken calldata _reSDLToken + ) external override onlyBridge { + sdlToken.safeTransfer(sdlPool, _reSDLToken.amount); + ISDLPoolPrimary(sdlPool).handleIncomingRESDL(_receiver, _tokenId, _reSDLToken); + reSDLSupplyByChain[_sourceChainSelector] -= _reSDLToken.amount + _reSDLToken.boostAmount; } /** @@ -311,14 +288,14 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { /** * @notice Processes a received message * @dev handles incoming updates from a secondary chain and sends an update in response - * @param _any2EvmMessage CCIP message + * @param _message CCIP message **/ - function _ccipReceive(Client.Any2EVMMessage memory _any2EvmMessage) internal override { - address sender = abi.decode(_any2EvmMessage.sender, (address)); - uint64 sourceChainSelector = _any2EvmMessage.sourceChainSelector; + function _ccipReceive(Client.Any2EVMMessage memory _message) internal override { + address sender = abi.decode(_message.sender, (address)); + uint64 sourceChainSelector = _message.sourceChainSelector; if (sender != whitelistedDestinations[sourceChainSelector]) revert SenderNotAuthorized(); - (uint256 numNewRESDLTokens, int256 totalRESDLSupplyChange) = abi.decode(_any2EvmMessage.data, (uint256, int256)); + (uint256 numNewRESDLTokens, int256 totalRESDLSupplyChange) = abi.decode(_message.data, (uint256, int256)); if (totalRESDLSupplyChange > 0) { reSDLSupplyByChain[sourceChainSelector] += uint256(totalRESDLSupplyChange); @@ -330,7 +307,7 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { _ccipSendUpdate(sourceChainSelector, mintStartIndex); - emit MessageReceived(_any2EvmMessage.messageId, sourceChainSelector); + emit MessageReceived(_message.messageId, sourceChainSelector); } /** diff --git a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol index 798beef8..4f398ca9 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.15; import "./base/SDLPoolCCIPController.sol"; -import "../interfaces/ISDLPool.sol"; interface ISDLPoolSecondary is ISDLPool { function handleOutgoingUpdate() external returns (uint256, int256); @@ -75,56 +74,34 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { * @notice Handles the outgoing transfer of an reSDL token to the primary chain * @param _sender sender of the transfer * @param _tokenId id of token + * @return the destination address * @return the token being transferred **/ function handleOutgoingRESDL( uint64, address _sender, uint256 _tokenId - ) - external - onlyBridge - returns ( - uint256, - uint256, - uint64, - uint64, - uint64 - ) - { - return ISDLPoolSecondary(sdlPool).handleOutgoingRESDL(_sender, _tokenId, reSDLTokenBridge); + ) external override onlyBridge returns (address, ISDLPool.RESDLToken memory) { + return ( + primaryChainDestination, + ISDLPoolSecondary(sdlPool).handleOutgoingRESDL(_sender, _tokenId, reSDLTokenBridge) + ); } /** * @notice Handles the incoming transfer of an reSDL token from the primary chain * @param _receiver receiver of the transfer * @param _tokenId id of reSDL token - * @param _amount amount of underlying SDL - * @param _boostAmount reSDL boost amount - * @param _startTime start time of the lock - * @param _duration duration of the lock - * @param _expiry expiry time of the lock + * @param _reSDLToken reSDL token **/ function handleIncomingRESDL( uint64, address _receiver, uint256 _tokenId, - uint256 _amount, - uint256 _boostAmount, - uint64 _startTime, - uint64 _duration, - uint64 _expiry - ) external onlyBridge { - sdlToken.safeTransferFrom(reSDLTokenBridge, sdlPool, _amount); - ISDLPoolSecondary(sdlPool).handleIncomingRESDL( - _receiver, - _tokenId, - _amount, - _boostAmount, - _startTime, - _duration, - _expiry - ); + ISDLPool.RESDLToken calldata _reSDLToken + ) external override onlyBridge { + sdlToken.safeTransferFrom(reSDLTokenBridge, sdlPool, _reSDLToken.amount); + ISDLPoolSecondary(sdlPool).handleIncomingRESDL(_receiver, _tokenId, _reSDLToken); } /** @@ -168,30 +145,30 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { /** * @notice Processes a received message * @dev handles incoming updates and reward distributions from the primary chain - * @param _any2EvmMessage CCIP message + * @param _message CCIP message **/ - function _ccipReceive(Client.Any2EVMMessage memory _any2EvmMessage) internal override { - address sender = abi.decode(_any2EvmMessage.sender, (address)); - uint64 sourceChainSelector = _any2EvmMessage.sourceChainSelector; + function _ccipReceive(Client.Any2EVMMessage memory _message) internal override { + address sender = abi.decode(_message.sender, (address)); + uint64 sourceChainSelector = _message.sourceChainSelector; if (sourceChainSelector != primaryChainSelector || sender != primaryChainDestination) revert SenderNotAuthorized(); - if (_any2EvmMessage.data.length == 0) { - uint256 numRewardTokens = _any2EvmMessage.destTokenAmounts.length; + if (_message.data.length == 0) { + uint256 numRewardTokens = _message.destTokenAmounts.length; address[] memory rewardTokens = new address[](numRewardTokens); if (numRewardTokens != 0) { for (uint256 i = 0; i < numRewardTokens; ++i) { - rewardTokens[i] = _any2EvmMessage.destTokenAmounts[i].token; - IERC20(rewardTokens[i]).safeTransfer(sdlPool, _any2EvmMessage.destTokenAmounts[i].amount); + rewardTokens[i] = _message.destTokenAmounts[i].token; + IERC20(rewardTokens[i]).safeTransfer(sdlPool, _message.destTokenAmounts[i].amount); } ISDLPoolSecondary(sdlPool).distributeTokens(rewardTokens); if (ISDLPoolSecondary(sdlPool).shouldUpdate()) shouldUpdate = true; } } else { - uint256 mintStartIndex = abi.decode(_any2EvmMessage.data, (uint256)); + uint256 mintStartIndex = abi.decode(_message.data, (uint256)); ISDLPoolSecondary(sdlPool).handleIncomingUpdate(mintStartIndex); } - emit MessageReceived(_any2EvmMessage.messageId, sourceChainSelector); + emit MessageReceived(_message.messageId, sourceChainSelector); } /** diff --git a/contracts/core/ccip/WrappedTokenBridge.sol b/contracts/core/ccip/WrappedTokenBridge.sol index e79ef199..1f5eeaf7 100644 --- a/contracts/core/ccip/WrappedTokenBridge.sol +++ b/contracts/core/ccip/WrappedTokenBridge.sol @@ -229,14 +229,14 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { /** * @notice Processes a received message - * @param _any2EvmMessage CCIP message + * @param _message CCIP message **/ - function _ccipReceive(Client.Any2EVMMessage memory _any2EvmMessage) internal override { - if (_any2EvmMessage.destTokenAmounts.length != 1) revert InvalidMessage(); + function _ccipReceive(Client.Any2EVMMessage memory _message) internal override { + if (_message.destTokenAmounts.length != 1) revert InvalidMessage(); - address tokenAddress = _any2EvmMessage.destTokenAmounts[0].token; - uint256 tokenAmount = _any2EvmMessage.destTokenAmounts[0].amount; - address receiver = abi.decode(_any2EvmMessage.data, (address)); + address tokenAddress = _message.destTokenAmounts[0].token; + uint256 tokenAmount = _message.destTokenAmounts[0].amount; + address receiver = abi.decode(_message.data, (address)); if (tokenAddress != address(wrappedToken) || receiver == address(0)) revert InvalidMessage(); @@ -246,9 +246,9 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { token.safeTransfer(receiver, amountToTransfer); emit TokensReceived( - _any2EvmMessage.messageId, - _any2EvmMessage.sourceChainSelector, - abi.decode(_any2EvmMessage.sender, (address)), + _message.messageId, + _message.sourceChainSelector, + abi.decode(_message.sender, (address)), receiver, tokenAmount ); diff --git a/contracts/core/ccip/base/SDLPoolCCIPController.sol b/contracts/core/ccip/base/SDLPoolCCIPController.sol index ed19d524..10c2c932 100644 --- a/contracts/core/ccip/base/SDLPoolCCIPController.sol +++ b/contracts/core/ccip/base/SDLPoolCCIPController.sol @@ -7,6 +7,9 @@ import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "../../interfaces/IRESDLTokenBridge.sol"; +import "../../interfaces/ISDLPool.sol"; + abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { using SafeERC20 for IERC20; @@ -21,18 +24,12 @@ abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { event MessageSent(bytes32 indexed messageId, uint64 indexed destinationChainSelector, uint256 fees); event MessageReceived(bytes32 indexed messageId, uint64 indexed destinationChainSelector); - error OnlyRESDLTokenBridge(); error AlreadyAdded(); error InvalidDestination(); error SenderNotAuthorized(); error FeeExceedsLimit(uint256 fee); error InvalidReceiver(); - modifier onlyBridge() { - if (msg.sender != reSDLTokenBridge) revert OnlyRESDLTokenBridge(); - _; - } - /** * @notice Initializes the contract * @param _router address of the CCIP router @@ -55,6 +52,60 @@ abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { linkToken.approve(_router, type(uint256).max); } + modifier onlyBridge() { + if (msg.sender != reSDLTokenBridge) revert SenderNotAuthorized(); + _; + } + + /** + * @notice Handles the outgoing transfer of an reSDL token to another chain + * @param _destinationChainSelector id of the destination chain + * @param _sender sender of the transfer + * @param _tokenId id of token + * @return the destination address + * @return the token being transferred + **/ + function handleOutgoingRESDL( + uint64 _destinationChainSelector, + address _sender, + uint256 _tokenId + ) external virtual returns (address, ISDLPool.RESDLToken memory); + + /** + * @notice Handles the incoming transfer of an reSDL token from another chain + * @param _sourceChainSelector id of the source chain + * @param _receiver receiver of the transfer + * @param _tokenId id of reSDL token + * @param _reSDLToken reSDL token + **/ + function handleIncomingRESDL( + uint64 _sourceChainSelector, + address _receiver, + uint256 _tokenId, + ISDLPool.RESDLToken calldata _reSDLToken + ) external virtual; + + function ccipSend(uint64 _destinationChainSelector, Client.EVM2AnyMessage calldata _evmToAnyMessage) + external + payable + onlyBridge + returns (bytes32) + { + if (msg.value != 0) { + return IRouterClient(this.getRouter()).ccipSend{value: msg.value}(_destinationChainSelector, _evmToAnyMessage); + } else { + return IRouterClient(this.getRouter()).ccipSend(_destinationChainSelector, _evmToAnyMessage); + } + } + + function ccipReceive(Client.Any2EVMMessage calldata _message) external override onlyRouter { + if (_message.destTokenAmounts.length == 1 && _message.destTokenAmounts[0].token == address(sdlToken)) { + IRESDLTokenBridge(reSDLTokenBridge).ccipReceive(_message); + } else { + _ccipReceive(_message); + } + } + /** * @notice Recovers tokens that were accidentally sent to this contract * @param _tokens list of tokens to recover diff --git a/contracts/core/interfaces/IRESDLTokenBridge.sol b/contracts/core/interfaces/IRESDLTokenBridge.sol new file mode 100644 index 00000000..4b396339 --- /dev/null +++ b/contracts/core/interfaces/IRESDLTokenBridge.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.15; + +import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; + +interface IRESDLTokenBridge { + function ccipReceive(Client.Any2EVMMessage calldata _anyToEvmMessage) external; +} diff --git a/contracts/core/interfaces/ISDLPool.sol b/contracts/core/interfaces/ISDLPool.sol index c1a1de46..a5910945 100644 --- a/contracts/core/interfaces/ISDLPool.sol +++ b/contracts/core/interfaces/ISDLPool.sol @@ -4,6 +4,14 @@ pragma solidity 0.8.15; import "./IRewardsPoolController.sol"; interface ISDLPool is IRewardsPoolController { + struct RESDLToken { + uint256 amount; + uint256 boostAmount; + uint64 startTime; + uint64 duration; + uint64 expiry; + } + function effectiveBalanceOf(address _account) external view returns (uint256); function ownerOf(uint256 _lockId) external view returns (address); @@ -12,25 +20,13 @@ interface ISDLPool is IRewardsPoolController { function handleOutgoingRESDL( address _sender, - uint256 _lockId, + uint256 _reSDLToken, address _sdlReceiver - ) - external - returns ( - uint256 _amount, - uint256 _boostAmount, - uint64 _startTime, - uint64 _duration, - uint64 _expiry - ); + ) external returns (RESDLToken memory); function handleIncomingRESDL( address _receiver, - uint256 _lockId, - uint256 _amount, - uint256 _boostAmount, - uint64 _startTime, - uint64 _duration, - uint64 _expiry + uint256 _tokenId, + RESDLToken calldata _reSDLToken ) external; } diff --git a/contracts/core/interfaces/ISDLPoolCCIPController.sol b/contracts/core/interfaces/ISDLPoolCCIPController.sol index 6e3a9672..7a97095b 100644 --- a/contracts/core/interfaces/ISDLPoolCCIPController.sol +++ b/contracts/core/interfaces/ISDLPoolCCIPController.sol @@ -1,29 +1,27 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.15; +import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; +import "./ISDLPool.sol"; + interface ISDLPoolCCIPController { function handleOutgoingRESDL( uint64 _destinationChainSelector, address _sender, - uint256 _lockId - ) - external - returns ( - uint256 _amount, - uint256 _boostAmount, - uint64 _startTime, - uint64 _duration, - uint64 _expiry - ); + uint256 _tokenId + ) external returns (address destination, ISDLPool.RESDLToken memory reSDLToken); function handleIncomingRESDL( uint64 _sourceChainSelector, address _receiver, - uint256 _lockId, - uint256 _amount, - uint256 _boostAmount, - uint64 _startTime, - uint64 _duration, - uint64 _expiry + uint256 _tokenId, + ISDLPool.RESDLToken calldata _reSDLToken ) external; + + function getRouter() external view returns (address); + + function ccipSend(uint64 _destinationChainSelector, Client.EVM2AnyMessage calldata _evmToAnyMessage) + external + payable + returns (bytes32); } diff --git a/contracts/core/sdlPool/SDLPoolPrimary.sol b/contracts/core/sdlPool/SDLPoolPrimary.sol index 6ac9bbf3..86d7095e 100644 --- a/contracts/core/sdlPool/SDLPoolPrimary.sol +++ b/contracts/core/sdlPool/SDLPoolPrimary.sol @@ -202,28 +202,20 @@ contract SDLPoolPrimary is SDLPool { * @notice handles an incoming transfer of an reSDL lock from another chain * @param _receiver receiver of lock * @param _lockId id of lock - * @param _amount amount of underlying SDL - * @param _boostAmount reSDL boost amount - * @param _startTime start time of lock - * @param _duration duration of lock - * @param _expiry expiry time of lock + * @param _lock lock */ function handleIncomingRESDL( address _receiver, uint256 _lockId, - uint256 _amount, - uint256 _boostAmount, - uint64 _startTime, - uint64 _duration, - uint64 _expiry + Lock calldata _lock ) external onlyCCIPController updateRewards(_receiver) updateRewards(ccipController) { if (lockOwners[_lockId] != address(0)) revert InvalidLockId(); - locks[_lockId] = Lock(_amount, _boostAmount, _startTime, _duration, _expiry); + locks[_lockId] = Lock(_lock.amount, _lock.boostAmount, _lock.startTime, _lock.duration, _lock.expiry); lockOwners[_lockId] = _receiver; balances[_receiver] += 1; - uint256 totalAmount = _amount + _boostAmount; + uint256 totalAmount = _lock.amount + _lock.boostAmount; effectiveBalances[_receiver] += totalAmount; effectiveBalances[ccipController] -= totalAmount; diff --git a/contracts/core/sdlPool/SDLPoolSecondary.sol b/contracts/core/sdlPool/SDLPoolSecondary.sol index ce79fbbb..27150425 100644 --- a/contracts/core/sdlPool/SDLPoolSecondary.sol +++ b/contracts/core/sdlPool/SDLPoolSecondary.sol @@ -284,28 +284,20 @@ contract SDLPoolSecondary is SDLPool { * @notice handles the incoming transfer of an reSDL lock from another chain * @param _receiver receiver of the transfer * @param _lockId id of lock - * @param _amount amount of underlying SDL - * @param _boostAmount reSDL boost amount - * @param _startTime start time of the lock - * @param _duration duration of the lock - * @param _expiry expiry time of the lock + * @param _lock lock **/ function handleIncomingRESDL( address _receiver, uint256 _lockId, - uint256 _amount, - uint256 _boostAmount, - uint64 _startTime, - uint64 _duration, - uint64 _expiry + Lock calldata _lock ) external onlyCCIPController updateRewards(_receiver) { if (lockOwners[_lockId] != address(0)) revert InvalidLockId(); - locks[_lockId] = Lock(_amount, _boostAmount, _startTime, _duration, _expiry); + locks[_lockId] = Lock(_lock.amount, _lock.boostAmount, _lock.startTime, _lock.duration, _lock.expiry); lockOwners[_lockId] = _receiver; balances[_receiver] += 1; - uint256 totalAmount = _amount + _boostAmount; + uint256 totalAmount = _lock.amount + _lock.boostAmount; effectiveBalances[_receiver] += totalAmount; totalEffectiveBalance += totalAmount; diff --git a/contracts/core/test/SDLPoolCCIPControllerMock.sol b/contracts/core/test/SDLPoolCCIPControllerMock.sol index 021f7537..26bbdfff 100644 --- a/contracts/core/test/SDLPoolCCIPControllerMock.sol +++ b/contracts/core/test/SDLPoolCCIPControllerMock.sol @@ -28,32 +28,18 @@ contract SDLPoolCCIPControllerMock { uint64, address _sender, uint256 _tokenId - ) - external - onlyBridge - returns ( - uint256, - uint256, - uint64, - uint64, - uint64 - ) - { - return sdlPool.handleOutgoingRESDL(_sender, _tokenId, reSDLTokenBridge); + ) external onlyBridge returns (address, ISDLPool.RESDLToken memory) { + return (address(0), sdlPool.handleOutgoingRESDL(_sender, _tokenId, reSDLTokenBridge)); } function handleIncomingRESDL( uint64, address _receiver, uint256 _tokenId, - uint256 _amount, - uint256 _boostAmount, - uint64 _startTime, - uint64 _duration, - uint64 _expiry + ISDLPool.RESDLToken calldata _reSDLToken ) external onlyBridge { - sdlToken.safeTransferFrom(reSDLTokenBridge, address(sdlPool), _amount); - sdlPool.handleIncomingRESDL(_receiver, _tokenId, _amount, _boostAmount, _startTime, _duration, _expiry); + sdlToken.safeTransferFrom(reSDLTokenBridge, address(sdlPool), _reSDLToken.amount); + sdlPool.handleIncomingRESDL(_receiver, _tokenId, _reSDLToken); } function setRESDLTokenBridge(address _reSDLTokenBridge) external { From 42c2f821c3206d478327dff53bfeb5f9d4bdfebe Mon Sep 17 00:00:00 2001 From: BkChoy Date: Wed, 13 Dec 2023 10:13:04 -0500 Subject: [PATCH 26/42] updated tests --- contracts/core/RewardsInitiator.sol | 2 +- contracts/core/base/RewardsPoolController.sol | 11 +- .../ccip/SDLPoolCCIPControllerPrimary.sol | 15 +- .../ccip/SDLPoolCCIPControllerSecondary.sol | 23 +- .../core/ccip/base/SDLPoolCCIPController.sol | 9 + contracts/core/sdlPool/base/SDLPool.sol | 4 +- .../core/test/SDLPoolCCIPControllerMock.sol | 6 + hardhat.config.ts | 2 +- test/core/ccip/resdl-token-bridge.test.ts | 106 +++---- .../sdl-pool-ccip-controller-primary.test.ts | 84 +++--- ...sdl-pool-ccip-controller-secondary.test.ts | 115 ++++++-- test/core/ccip/wrapped-token-bridge.test.ts | 15 +- test/core/priorityPool/priority-pool.test.ts | 1 + ...eper.test.ts => rewards-initiator.test.ts} | 60 +++- test/core/sdlPool/sdl-pool-primary.test.ts | 46 ++- test/core/sdlPool/sdl-pool-secondary.test.ts | 77 ++++- test/core/staking-pool.test.ts | 1 + test/core/wrapped-sd-token.test.ts | 1 + test/ethStaking/eth-staking-strategy.test.ts | 1 + test/linkStaking/operator-vcs-1.5.test.ts | 276 ------------------ test/linkStaking/operator-vcs.test.ts | 1 + 21 files changed, 421 insertions(+), 435 deletions(-) rename test/core/{slashing-keeper.test.ts => rewards-initiator.test.ts} (62%) delete mode 100644 test/linkStaking/operator-vcs-1.5.test.ts diff --git a/contracts/core/RewardsInitiator.sol b/contracts/core/RewardsInitiator.sol index 9896a55f..9994bf7c 100644 --- a/contracts/core/RewardsInitiator.sol +++ b/contracts/core/RewardsInitiator.sol @@ -11,7 +11,7 @@ import "./interfaces/ISDLPoolCCIPControllerPrimary.sol"; * @dev Chainlink automation should call updateRewards periodically under normal circumstances and call performUpkeep * in the case of a negative rebase in the staking pool */ -contract SlashingKeeper { +contract RewardsInitiator { IStakingPool public stakingPool; ISDLPoolCCIPControllerPrimary public sdlPoolCCIPController; diff --git a/contracts/core/base/RewardsPoolController.sol b/contracts/core/base/RewardsPoolController.sol index 024d002b..8a08c24b 100644 --- a/contracts/core/base/RewardsPoolController.sol +++ b/contracts/core/base/RewardsPoolController.sol @@ -22,6 +22,9 @@ abstract contract RewardsPoolController is UUPSUpgradeable, OwnableUpgradeable { event AddToken(address indexed token, address rewardsPool); event RemoveToken(address indexed token, address rewardsPool); + error InvalidToken(); + error NothingToDistribute(); + function __RewardsPoolController_init() public onlyInitializing { __Ownable_init(); __UUPSUpgradeable_init(); @@ -107,11 +110,11 @@ abstract contract RewardsPoolController is UUPSUpgradeable, OwnableUpgradeable { * @param _token token address */ function distributeToken(address _token) public { - require(isTokenSupported(_token), "Token not supported"); + if (!isTokenSupported(_token)) revert InvalidToken(); IERC20Upgradeable token = IERC20Upgradeable(_token); uint256 balance = token.balanceOf(address(this)); - require(balance > 0, "Cannot distribute zero balance"); + if (balance == 0) revert NothingToDistribute(); token.safeTransfer(address(tokenPools[_token]), balance); tokenPools[_token].distributeRewards(); @@ -149,7 +152,7 @@ abstract contract RewardsPoolController is UUPSUpgradeable, OwnableUpgradeable { * @param _rewardsPool token rewards pool to add **/ function addToken(address _token, address _rewardsPool) public virtual onlyOwner { - require(!isTokenSupported(_token), "Token is already supported"); + if (isTokenSupported(_token)) revert InvalidToken(); tokenPools[_token] = IRewardsPool(_rewardsPool); tokens.push(_token); @@ -166,7 +169,7 @@ abstract contract RewardsPoolController is UUPSUpgradeable, OwnableUpgradeable { * @param _token address of token **/ function removeToken(address _token) external onlyOwner { - require(isTokenSupported(_token), "Token is not supported"); + if (!isTokenSupported(_token)) revert InvalidToken(); IRewardsPool rewardsPool = tokenPools[_token]; delete (tokenPools[_token]); diff --git a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol index 326880e9..c7555ebe 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol @@ -105,10 +105,11 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { address _sender, uint256 _tokenId ) external override onlyBridge returns (address, ISDLPool.RESDLToken memory) { + if (whitelistedDestinations[_destinationChainSelector] == address(0)) revert InvalidDestination(); ISDLPool.RESDLToken memory reSDLToken = ISDLPoolPrimary(sdlPool).handleOutgoingRESDL( _sender, _tokenId, - reSDLTokenBridge + address(this) ); reSDLSupplyByChain[_destinationChainSelector] += reSDLToken.amount + reSDLToken.boostAmount; return (whitelistedDestinations[_destinationChainSelector], reSDLToken); @@ -291,9 +292,7 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { * @param _message CCIP message **/ function _ccipReceive(Client.Any2EVMMessage memory _message) internal override { - address sender = abi.decode(_message.sender, (address)); uint64 sourceChainSelector = _message.sourceChainSelector; - if (sender != whitelistedDestinations[sourceChainSelector]) revert SenderNotAuthorized(); (uint256 numNewRESDLTokens, int256 totalRESDLSupplyChange) = abi.decode(_message.data, (uint256, int256)); @@ -366,4 +365,14 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { return evm2AnyMessage; } + + /** + * @notice Verifies the sender of a CCIP message is whitelisted + * @param _message CCIP message + **/ + function _verifyCCIPSender(Client.Any2EVMMessage memory _message) internal view override { + address sender = abi.decode(_message.sender, (address)); + uint64 sourceChainSelector = _message.sourceChainSelector; + if (sender != whitelistedDestinations[sourceChainSelector]) revert SenderNotAuthorized(); + } } diff --git a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol index 4f398ca9..591b752e 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol @@ -82,10 +82,7 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { address _sender, uint256 _tokenId ) external override onlyBridge returns (address, ISDLPool.RESDLToken memory) { - return ( - primaryChainDestination, - ISDLPoolSecondary(sdlPool).handleOutgoingRESDL(_sender, _tokenId, reSDLTokenBridge) - ); + return (primaryChainDestination, ISDLPoolSecondary(sdlPool).handleOutgoingRESDL(_sender, _tokenId, address(this))); } /** @@ -100,7 +97,7 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { uint256 _tokenId, ISDLPool.RESDLToken calldata _reSDLToken ) external override onlyBridge { - sdlToken.safeTransferFrom(reSDLTokenBridge, sdlPool, _reSDLToken.amount); + sdlToken.safeTransfer(sdlPool, _reSDLToken.amount); ISDLPoolSecondary(sdlPool).handleIncomingRESDL(_receiver, _tokenId, _reSDLToken); } @@ -148,10 +145,6 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { * @param _message CCIP message **/ function _ccipReceive(Client.Any2EVMMessage memory _message) internal override { - address sender = abi.decode(_message.sender, (address)); - uint64 sourceChainSelector = _message.sourceChainSelector; - if (sourceChainSelector != primaryChainSelector || sender != primaryChainDestination) revert SenderNotAuthorized(); - if (_message.data.length == 0) { uint256 numRewardTokens = _message.destTokenAmounts.length; address[] memory rewardTokens = new address[](numRewardTokens); @@ -168,7 +161,7 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { ISDLPoolSecondary(sdlPool).handleIncomingUpdate(mintStartIndex); } - emit MessageReceived(_message.messageId, sourceChainSelector); + emit MessageReceived(_message.messageId, _message.sourceChainSelector); } /** @@ -195,4 +188,14 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { return evm2AnyMessage; } + + /** + * @notice Verifies the sender of a CCIP message is whitelisted + * @param _message CCIP message + **/ + function _verifyCCIPSender(Client.Any2EVMMessage memory _message) internal view override { + address sender = abi.decode(_message.sender, (address)); + uint64 sourceChainSelector = _message.sourceChainSelector; + if (sourceChainSelector != primaryChainSelector || sender != primaryChainDestination) revert SenderNotAuthorized(); + } } diff --git a/contracts/core/ccip/base/SDLPoolCCIPController.sol b/contracts/core/ccip/base/SDLPoolCCIPController.sol index 10c2c932..7fe4d529 100644 --- a/contracts/core/ccip/base/SDLPoolCCIPController.sol +++ b/contracts/core/ccip/base/SDLPoolCCIPController.sol @@ -50,6 +50,7 @@ abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { sdlPool = _sdlPool; maxLINKFee = _maxLINKFee; linkToken.approve(_router, type(uint256).max); + sdlToken.approve(_router, type(uint256).max); } modifier onlyBridge() { @@ -99,6 +100,8 @@ abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { } function ccipReceive(Client.Any2EVMMessage calldata _message) external override onlyRouter { + _verifyCCIPSender(_message); + if (_message.destTokenAmounts.length == 1 && _message.destTokenAmounts[0].token == address(sdlToken)) { IRESDLTokenBridge(reSDLTokenBridge).ccipReceive(_message); } else { @@ -135,4 +138,10 @@ abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { function setRESDLTokenBridge(address _reSDLTokenBridge) external onlyOwner { reSDLTokenBridge = _reSDLTokenBridge; } + + /** + * @notice Verifies the sender of a CCIP message is whitelisted + * @param _message CCIP message + **/ + function _verifyCCIPSender(Client.Any2EVMMessage memory _message) internal view virtual; } diff --git a/contracts/core/sdlPool/base/SDLPool.sol b/contracts/core/sdlPool/base/SDLPool.sol index 2409607c..f036770a 100644 --- a/contracts/core/sdlPool/base/SDLPool.sol +++ b/contracts/core/sdlPool/base/SDLPool.sol @@ -72,7 +72,6 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp error TransferToCCIPController(); error ApprovalToCurrentOwner(); error ApprovalToCaller(); - error OnlyCCIPController(); error InvalidValue(); error InvalidParams(); error UnauthorizedToken(); @@ -83,7 +82,6 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp error DuplicateContract(); error ContractNotFound(); error UnlockAlreadyInitiated(); - error InvalidToken(); /** * @notice initializes contract @@ -117,7 +115,7 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp * @notice reverts if sender is not the CCIP controller **/ modifier onlyCCIPController() { - if (msg.sender != ccipController) revert OnlyCCIPController(); + if (msg.sender != ccipController) revert SenderNotAuthorized(); _; } diff --git a/contracts/core/test/SDLPoolCCIPControllerMock.sol b/contracts/core/test/SDLPoolCCIPControllerMock.sol index 26bbdfff..24883909 100644 --- a/contracts/core/test/SDLPoolCCIPControllerMock.sol +++ b/contracts/core/test/SDLPoolCCIPControllerMock.sol @@ -12,6 +12,8 @@ contract SDLPoolCCIPControllerMock { ISDLPool public sdlPool; address public reSDLTokenBridge; + uint256 public rewardsDistributed; + error OnlyRESDLTokenBridge(); modifier onlyBridge() { @@ -42,6 +44,10 @@ contract SDLPoolCCIPControllerMock { sdlPool.handleIncomingRESDL(_receiver, _tokenId, _reSDLToken); } + function distributeRewards() external { + rewardsDistributed++; + } + function setRESDLTokenBridge(address _reSDLTokenBridge) external { reSDLTokenBridge = _reSDLTokenBridge; } diff --git a/hardhat.config.ts b/hardhat.config.ts index 20c82185..44fd5fe2 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -76,7 +76,7 @@ const config: HardhatUserConfig = { settings: { optimizer: { enabled: true, - runs: 200, + runs: 115, }, }, }, diff --git a/test/core/ccip/resdl-token-bridge.test.ts b/test/core/ccip/resdl-token-bridge.test.ts index 1e6ba8f5..cf37d654 100644 --- a/test/core/ccip/resdl-token-bridge.test.ts +++ b/test/core/ccip/resdl-token-bridge.test.ts @@ -8,8 +8,8 @@ import { CCIPTokenPoolMock, WrappedNative, RESDLTokenBridge, - SDLPoolCCIPControllerMock, SDLPoolPrimary, + SDLPoolCCIPControllerPrimary, } from '../../../typechain-types' import { time } from '@nomicfoundation/hardhat-network-helpers' import { Signer } from 'ethers' @@ -20,6 +20,7 @@ describe('RESDLTokenBridge', () => { let token2: ERC677 let bridge: RESDLTokenBridge let sdlPool: SDLPoolPrimary + let sdlPoolCCIPController: SDLPoolCCIPControllerPrimary let onRamp: CCIPOnRampMock let offRamp: CCIPOffRampMock let tokenPool: CCIPTokenPoolMock @@ -62,13 +63,15 @@ describe('RESDLTokenBridge', () => { sdlToken.address, boostController.address, ])) as SDLPoolPrimary - let sdlPoolCCIPController = (await deploy('SDLPoolCCIPControllerMock', [ + sdlPoolCCIPController = (await deploy('SDLPoolCCIPControllerPrimary', [ + router.address, + linkToken.address, sdlToken.address, sdlPool.address, - ])) as SDLPoolCCIPControllerMock + toEther(10), + ])) as SDLPoolCCIPControllerPrimary bridge = (await deploy('RESDLTokenBridge', [ - router.address, linkToken.address, sdlToken.address, sdlPool.address, @@ -78,7 +81,8 @@ describe('RESDLTokenBridge', () => { await sdlPoolCCIPController.setRESDLTokenBridge(bridge.address) await sdlPool.setCCIPController(sdlPoolCCIPController.address) await linkToken.approve(bridge.address, ethers.constants.MaxUint256) - await bridge.addWhitelistedDestination(77, accounts[0]) + await bridge.setExtraArgs(77, '0x11') + await sdlPoolCCIPController.addWhitelistedChain(77, accounts[6], '0x', '0x') await sdlToken.transfer(accounts[1], toEther(200)) await sdlToken.transferAndCall( @@ -94,10 +98,10 @@ describe('RESDLTokenBridge', () => { }) it('getFee should work correctly', async () => { - assert.equal(fromEther(await bridge.getFee(77, false, '0x')), 2) - assert.equal(fromEther(await bridge.getFee(77, true, '0x')), 3) - await expect(bridge.getFee(78, false, '0x')).to.be.reverted - await expect(bridge.getFee(78, true, '0x')).to.be.reverted + assert.equal(fromEther(await bridge.getFee(77, false)), 2) + assert.equal(fromEther(await bridge.getFee(77, true)), 3) + await expect(bridge.getFee(78, false)).to.be.reverted + await expect(bridge.getFee(78, true)).to.be.reverted }) it('transferRESDL should work correctly with LINK fee', async () => { @@ -108,7 +112,7 @@ describe('RESDLTokenBridge', () => { let preFeeBalance = await linkToken.balanceOf(accounts[0]) - await bridge.transferRESDL(77, accounts[4], 2, false, toEther(10), '0x') + await bridge.transferRESDL(77, accounts[4], 2, false, toEther(10)) let lastRequestData = await onRamp.getLastRequestData() let lastRequestMsg = await onRamp.getLastRequestMessage() @@ -116,11 +120,11 @@ describe('RESDLTokenBridge', () => { assert.equal(fromEther(preFeeBalance.sub(await linkToken.balanceOf(accounts[0]))), 2) assert.equal(fromEther(lastRequestData[0]), 2) - assert.equal(lastRequestData[1], bridge.address) + assert.equal(lastRequestData[1], sdlPoolCCIPController.address) assert.equal( ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], - accounts[0] + accounts[6] ) assert.deepEqual( ethers.utils.defaultAbiCoder @@ -140,11 +144,12 @@ describe('RESDLTokenBridge', () => { [[sdlToken.address, 1000]] ) assert.equal(lastRequestMsg[3], linkToken.address) + assert.equal(lastRequestMsg[4], '0x11') await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') - await expect( - bridge.transferRESDL(77, accounts[4], 1, false, toEther(1), '0x') - ).to.be.revertedWith('FeeExceedsLimit()') + await expect(bridge.transferRESDL(77, accounts[4], 1, false, toEther(1))).to.be.revertedWith( + 'FeeExceedsLimit()' + ) await sdlToken.transferAndCall( sdlPool.address, @@ -155,7 +160,7 @@ describe('RESDLTokenBridge', () => { preFeeBalance = await linkToken.balanceOf(accounts[0]) - await bridge.transferRESDL(77, accounts[5], 3, false, toEther(10), '0x') + await bridge.transferRESDL(77, accounts[5], 3, false, toEther(10)) lastRequestData = await onRamp.getLastRequestData() lastRequestMsg = await onRamp.getLastRequestMessage() @@ -163,11 +168,11 @@ describe('RESDLTokenBridge', () => { assert.equal(fromEther(preFeeBalance.sub(await linkToken.balanceOf(accounts[0]))), 2) assert.equal(fromEther(lastRequestData[0]), 2) - assert.equal(lastRequestData[1], bridge.address) + assert.equal(lastRequestData[1], sdlPoolCCIPController.address) assert.equal( ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], - accounts[0] + accounts[6] ) assert.deepEqual( ethers.utils.defaultAbiCoder @@ -187,6 +192,7 @@ describe('RESDLTokenBridge', () => { [[sdlToken.address, 500]] ) assert.equal(lastRequestMsg[3], linkToken.address) + assert.equal(lastRequestMsg[4], '0x11') await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') }) @@ -195,7 +201,7 @@ describe('RESDLTokenBridge', () => { let preFeeBalance = await ethers.provider.getBalance(accounts[0]) - await bridge.transferRESDL(77, accounts[4], 2, true, toEther(10), '0x', { value: toEther(10) }) + await bridge.transferRESDL(77, accounts[4], 2, true, toEther(10), { value: toEther(10) }) let lastRequestData = await onRamp.getLastRequestData() let lastRequestMsg = await onRamp.getLastRequestMessage() @@ -205,11 +211,11 @@ describe('RESDLTokenBridge', () => { 3 ) assert.equal(fromEther(lastRequestData[0]), 3) - assert.equal(lastRequestData[1], bridge.address) + assert.equal(lastRequestData[1], sdlPoolCCIPController.address) assert.equal( ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], - accounts[0] + accounts[6] ) assert.deepEqual( ethers.utils.defaultAbiCoder @@ -229,25 +235,26 @@ describe('RESDLTokenBridge', () => { [[sdlToken.address, 1000]] ) assert.equal(lastRequestMsg[3], wrappedNative.address) + assert.equal(lastRequestMsg[4], '0x11') await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') }) it('transferRESDL validation should work correctly', async () => { await expect( - bridge.connect(signers[1]).transferRESDL(77, accounts[4], 1, false, toEther(10), '0x') + bridge.connect(signers[1]).transferRESDL(77, accounts[4], 1, false, toEther(10)) ).to.be.revertedWith('SenderNotAuthorized()') await expect( - bridge.transferRESDL(77, ethers.constants.AddressZero, 1, false, toEther(10), '0x') + bridge.transferRESDL(77, ethers.constants.AddressZero, 1, false, toEther(10)) ).to.be.revertedWith('InvalidReceiver()') - await expect( - bridge.transferRESDL(78, accounts[4], 1, false, toEther(10), '0x') - ).to.be.revertedWith('InvalidDestination()') + await expect(bridge.transferRESDL(78, accounts[4], 1, false, toEther(10))).to.be.revertedWith( + 'InvalidDestination()' + ) - bridge.transferRESDL(77, accounts[4], 1, false, toEther(10), '0x') + bridge.transferRESDL(77, accounts[4], 1, false, toEther(10)) }) it('ccipReceive should work correctly', async () => { - await bridge.transferRESDL(77, accounts[4], 2, true, toEther(10), '0x', { value: toEther(10) }) + await bridge.transferRESDL(77, accounts[4], 2, true, toEther(10), { value: toEther(10) }) let success: any = await offRamp .connect(signers[1]) @@ -258,21 +265,23 @@ describe('RESDLTokenBridge', () => { ['address', 'uint256', 'uint256', 'uint256', 'uint64', 'uint64', 'uint64'], [accounts[5], 2, toEther(25), toEther(25), 1000, 3000, 8000] ), - bridge.address, + sdlPoolCCIPController.address, [{ token: sdlToken.address, amount: toEther(25) }] ) assert.equal(success, false) - await offRamp.executeSingleMessage( - ethers.utils.formatBytes32String('messageId'), - 77, - ethers.utils.defaultAbiCoder.encode( - ['address', 'uint256', 'uint256', 'uint256', 'uint64', 'uint64', 'uint64'], - [accounts[5], 2, toEther(25), toEther(25), 1000, 3000, 8000] - ), - bridge.address, - [{ token: sdlToken.address, amount: toEther(25) }] - ) + await offRamp + .connect(signers[6]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + ethers.utils.defaultAbiCoder.encode( + ['address', 'uint256', 'uint256', 'uint256', 'uint64', 'uint64', 'uint64'], + [accounts[5], 2, toEther(25), toEther(25), 1000, 3000, 8000] + ), + sdlPoolCCIPController.address, + [{ token: sdlToken.address, amount: toEther(25) }] + ) assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 225) assert.equal(await sdlPool.ownerOf(2), accounts[5]) @@ -296,20 +305,11 @@ describe('RESDLTokenBridge', () => { ) }) - it('should be able to add/remove whitelisted destinations', async () => { - await expect( - bridge.addWhitelistedDestination(10, ethers.constants.AddressZero) - ).to.be.revertedWith('InvalidDestination()') - await expect(bridge.removeWhitelistedDestination(10)).to.be.revertedWith('AlreadyRemoved()') - - await bridge.addWhitelistedDestination(10, accounts[0]) - - assert.equal(await bridge.whitelistedDestinations(10), accounts[0]) - await expect(bridge.addWhitelistedDestination(10, accounts[1])).to.be.revertedWith( - 'AlreadyAdded()' - ) + it('should be able to set extra args', async () => { + await bridge.setExtraArgs(10, '0x22') + assert.equal(await bridge.extraArgsByChain(10), '0x22') - await bridge.removeWhitelistedDestination(10) - assert.equal(await bridge.whitelistedDestinations(10), ethers.constants.AddressZero) + await bridge.setExtraArgs(77, '0x33') + assert.equal(await bridge.extraArgsByChain(77), '0x33') }) }) diff --git a/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts b/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts index 5fbf8b99..4164f611 100644 --- a/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts +++ b/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts @@ -94,6 +94,8 @@ describe('SDLPoolCCIPControllerPrimary', () => { await sdlPool.setCCIPController(controller.address) await controller.setRESDLTokenBridge(accounts[5]) + await controller.setRewardsInitiator(accounts[0]) + await controller.addWhitelistedChain(77, accounts[4], '0x11', '0x22') }) it('handleOutgoingRESDL should work correctly', async () => { @@ -109,26 +111,33 @@ describe('SDLPoolCCIPControllerPrimary', () => { ).to.be.revertedWith('SenderNotAuthorized()') assert.deepEqual( - parseLock( - await controller.connect(signers[5]).callStatic.handleOutgoingRESDL(77, accounts[0], 3) - ), - { amount: 200, boostAmount: 200, startTime: ts, duration: 365 * 86400, expiry: 0 } + await controller + .connect(signers[5]) + .callStatic.handleOutgoingRESDL(77, accounts[0], 3) + .then((d: any) => [d[0], parseLock(d[1])]), + [ + accounts[4], + { amount: 200, boostAmount: 200, startTime: ts, duration: 365 * 86400, expiry: 0 }, + ] ) await controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[0], 3) - assert.equal(fromEther(await sdlToken.balanceOf(accounts[5])), 200) + assert.equal(fromEther(await sdlToken.balanceOf(controller.address)), 200) assert.equal(fromEther(await controller.reSDLSupplyByChain(77)), 400) await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') }) it('handleIncomingRESDL should work correctly', async () => { - await sdlToken.connect(signers[5]).approve(controller.address, toEther(100)) await controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[0], 1) - await controller - .connect(signers[5]) - .handleIncomingRESDL(77, accounts[3], 1, toEther(100), toEther(100), 111, 222, 0) - assert.equal(fromEther(await sdlToken.balanceOf(accounts[5])), 0) + await controller.connect(signers[5]).handleIncomingRESDL(77, accounts[3], 1, { + amount: toEther(100), + boostAmount: toEther(100), + startTime: 111, + duration: 222, + expiry: 0, + }) + assert.equal(fromEther(await sdlToken.balanceOf(controller.address)), 0) assert.equal(fromEther(await controller.reSDLSupplyByChain(77)), 0) assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 300) assert.equal(await sdlPool.ownerOf(1), accounts[3]) @@ -142,23 +151,24 @@ describe('SDLPoolCCIPControllerPrimary', () => { }) it('adding/removing whitelisted chains should work correctly', async () => { - await controller.addWhitelistedChain(77, accounts[5], '0x11') - await controller.addWhitelistedChain(88, accounts[6], '0x22') + await controller.addWhitelistedChain(88, accounts[6], '0x33', '0x44') assert.deepEqual( (await controller.getWhitelistedChains()).map((d) => d.toNumber()), [77, 88] ) - assert.equal(await controller.whitelistedDestinations(77), accounts[5]) + assert.equal(await controller.whitelistedDestinations(77), accounts[4]) assert.equal(await controller.whitelistedDestinations(88), accounts[6]) - assert.equal(await controller.extraArgsByChain(77), '0x11') - assert.equal(await controller.extraArgsByChain(88), '0x22') + assert.equal(await controller.updateExtraArgsByChain(77), '0x11') + assert.equal(await controller.rewardsExtraArgsByChain(77), '0x22') + assert.equal(await controller.updateExtraArgsByChain(88), '0x33') + assert.equal(await controller.rewardsExtraArgsByChain(88), '0x44') - await expect(controller.addWhitelistedChain(77, accounts[7], '0x11')).to.be.revertedWith( - 'AlreadyAdded()' - ) await expect( - controller.addWhitelistedChain(99, ethers.constants.AddressZero, '0x11') + controller.addWhitelistedChain(77, accounts[7], '0x11', '0x22') + ).to.be.revertedWith('AlreadyAdded()') + await expect( + controller.addWhitelistedChain(99, ethers.constants.AddressZero, '0x11', '0x22') ).to.be.revertedWith('InvalidDestination()') await controller.removeWhitelistedChain(77) @@ -167,7 +177,8 @@ describe('SDLPoolCCIPControllerPrimary', () => { [88] ) assert.equal(await controller.whitelistedDestinations(77), ethers.constants.AddressZero) - assert.equal(await controller.extraArgsByChain(77), '0x') + assert.equal(await controller.updateExtraArgsByChain(77), '0x') + assert.equal(await controller.rewardsExtraArgsByChain(77), '0x') await expect(controller.removeWhitelistedChain(77)).to.be.revertedWith('InvalidDestination()') }) @@ -176,18 +187,18 @@ describe('SDLPoolCCIPControllerPrimary', () => { let rewardsPool1 = await deploy('RewardsPool', [sdlPool.address, token1.address]) await sdlPool.addToken(token1.address, rewardsPool1.address) await controller.approveRewardTokens([token1.address, token2.address]) - await controller.addWhitelistedChain(77, accounts[6], '0x') await controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[0], 1) await token1.transferAndCall(rewardsPool1.address, toEther(50), '0x') - await controller.distributeRewards(['0x']) + await controller.distributeRewards() let requestData = await onRamp.getLastRequestData() let requestMsg: any = await onRamp.getLastRequestMessage() assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 98) assert.equal(fromEther(requestData[0]), 2) assert.equal(requestData[1], controller.address) - assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[6]) + assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[4]) assert.equal(requestMsg[3], linkToken.address) + assert.equal(requestMsg[4], '0x22') assert.deepEqual( requestMsg.tokenAmounts.map((d: any) => [d[0], fromEther(d[1])]), [[token1.address, 25]] @@ -210,7 +221,7 @@ describe('SDLPoolCCIPControllerPrimary', () => { let rewardsPool2 = await deploy('RewardsPool', [sdlPool.address, token2.address]) await sdlPool.addToken(token2.address, rewardsPool2.address) - await controller.addWhitelistedChain(88, accounts[7], '0x') + await controller.addWhitelistedChain(88, accounts[7], '0x', '0x33') await sdlToken.transferAndCall( sdlPool.address, toEther(400), @@ -219,15 +230,16 @@ describe('SDLPoolCCIPControllerPrimary', () => { await controller.connect(signers[5]).handleOutgoingRESDL(88, accounts[0], 3) await token1.transferAndCall(rewardsPool1.address, toEther(200), '0x') await token2.transferAndCall(rewardsPool2.address, toEther(300), '0x') - await controller.distributeRewards(['0x', '0x']) + await controller.distributeRewards() requestData = await onRamp.getLastRequestData() requestMsg = await onRamp.getLastRequestMessage() assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 94) assert.equal(fromEther(requestData[0]), 2) assert.equal(requestData[1], controller.address) - assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[6]) + assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[4]) assert.equal(requestMsg[3], linkToken.address) + assert.equal(requestMsg[4], '0x22') assert.deepEqual( requestMsg.tokenAmounts.map((d: any) => [d[0], fromEther(d[1])]), [ @@ -244,6 +256,7 @@ describe('SDLPoolCCIPControllerPrimary', () => { assert.equal(requestData[1], controller.address) assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[7]) assert.equal(requestMsg[3], linkToken.address) + assert.equal(requestMsg[4], '0x33') assert.deepEqual( requestMsg.tokenAmounts.map((d: any) => [d[0], fromEther(d[1])]), [ @@ -266,19 +279,18 @@ describe('SDLPoolCCIPControllerPrimary', () => { await sdlPool.addToken(token1.address, rewardsPool.address) await controller.approveRewardTokens([wToken.address]) await controller.setWrappedRewardToken(token1.address, wToken.address) - await controller.addWhitelistedChain(77, accounts[6], '0x') await onRamp.setTokenPool(wToken.address, wtokenPool.address) await offRamp.setTokenPool(wToken.address, wtokenPool.address) await controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[0], 1) await token1.transferAndCall(rewardsPool.address, toEther(500), '0x') - await controller.distributeRewards(['0x']) + await controller.distributeRewards() let requestData = await onRamp.getLastRequestData() let requestMsg: any = await onRamp.getLastRequestMessage() assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 98) assert.equal(fromEther(requestData[0]), 2) assert.equal(requestData[1], controller.address) - assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[6]) + assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[4]) assert.equal(requestMsg[3], linkToken.address) assert.deepEqual( requestMsg.tokenAmounts.map((d: any) => [d[0], fromEther(d[1])]), @@ -288,9 +300,8 @@ describe('SDLPoolCCIPControllerPrimary', () => { }) it('ccipReceive should work correctly', async () => { - await controller.addWhitelistedChain(77, accounts[5], '0x') await offRamp - .connect(signers[5]) + .connect(signers[4]) .executeSingleMessage( ethers.utils.formatBytes32String('messageId'), 77, @@ -308,12 +319,13 @@ describe('SDLPoolCCIPControllerPrimary', () => { assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 98) assert.equal(fromEther(requestData[0]), 2) assert.equal(requestData[1], controller.address) - assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[5]) + assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[4]) assert.equal(ethers.utils.defaultAbiCoder.decode(['uint256'], requestMsg[1])[0], 3) assert.equal(requestMsg[3], linkToken.address) + assert.equal(requestMsg[4], '0x11') await offRamp - .connect(signers[5]) + .connect(signers[4]) .executeSingleMessage( ethers.utils.formatBytes32String('messageId'), 77, @@ -331,11 +343,12 @@ describe('SDLPoolCCIPControllerPrimary', () => { assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 96) assert.equal(fromEther(requestData[0]), 2) assert.equal(requestData[1], controller.address) - assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[5]) + assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[4]) assert.equal(ethers.utils.defaultAbiCoder.decode(['uint256'], requestMsg[1])[0], 0) assert.equal(requestMsg[3], linkToken.address) + assert.equal(requestMsg[4], '0x11') - await controller.addWhitelistedChain(88, accounts[6], '0x') + await controller.addWhitelistedChain(88, accounts[6], '0x33', '0x') let onRamp88 = (await deploy('CCIPOnRampMock', [[], [], linkToken.address])) as CCIPOnRampMock let offRamp88 = (await deploy('CCIPOffRampMock', [router.address, [], []])) as CCIPOffRampMock await router.applyRampUpdates([[88, onRamp88.address]], [], [[88, offRamp88.address]]) @@ -361,6 +374,7 @@ describe('SDLPoolCCIPControllerPrimary', () => { assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[6]) assert.equal(ethers.utils.defaultAbiCoder.decode(['uint256'], requestMsg[1])[0].toNumber(), 6) assert.equal(requestMsg[3], linkToken.address) + assert.equal(requestMsg[4], '0x33') }) it('recoverTokens should work correctly', async () => { diff --git a/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts index 981010bc..5136e8dc 100644 --- a/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts +++ b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts @@ -67,6 +67,7 @@ describe('SDLPoolCCIPControllerSecondary', () => { 'reSDL', sdlToken.address, boostController.address, + 5, ])) as SDLPoolSecondary controller = (await deploy('SDLPoolCCIPControllerSecondary', [ router.address, @@ -76,7 +77,6 @@ describe('SDLPoolCCIPControllerSecondary', () => { 77, accounts[4], toEther(10), - 10000, '0x', ])) as SDLPoolCCIPControllerSecondary @@ -109,25 +109,25 @@ describe('SDLPoolCCIPControllerSecondary', () => { ).to.be.revertedWith('SenderNotAuthorized()') assert.deepEqual( - parseLock( - await controller.connect(signers[5]).callStatic.handleOutgoingRESDL(77, accounts[1], 2) - ), - { amount: 200, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 } + await controller + .connect(signers[5]) + .callStatic.handleOutgoingRESDL(77, accounts[1], 2) + .then((d: any) => [d[0], parseLock(d[1])]), + [accounts[4], { amount: 200, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }] ) await controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[1], 2) - assert.equal(fromEther(await sdlToken.balanceOf(accounts[5])), 200) + assert.equal(fromEther(await sdlToken.balanceOf(controller.address)), 200) await expect(sdlPool.ownerOf(2)).to.be.revertedWith('InvalidLockId()') }) it('handleIncomingRESDL should work correctly', async () => { - await sdlToken.transfer(accounts[5], toEther(300)) - await sdlToken.connect(signers[5]).approve(controller.address, toEther(300)) + await sdlToken.transfer(controller.address, toEther(300)) await controller .connect(signers[5]) - .handleIncomingRESDL(77, accounts[3], 7, toEther(300), toEther(200), 111, 222, 0) - assert.equal(fromEther(await sdlToken.balanceOf(accounts[5])), 0) + .handleIncomingRESDL(77, accounts[3], 7, [toEther(300), toEther(200), 111, 222, 0]) + assert.equal(fromEther(await sdlToken.balanceOf(controller.address)), 0) assert.equal(fromEther(await sdlToken.balanceOf(sdlPool.address)), 600) assert.equal(await sdlPool.ownerOf(7), accounts[3]) assert.deepEqual(parseLock((await sdlPool.getLocks([7]))[0]), { @@ -140,37 +140,58 @@ describe('SDLPoolCCIPControllerSecondary', () => { }) it('checkUpkeep should work correctly', async () => { + await token1.transfer(tokenPool.address, toEther(1000)) + let rewardsPool1 = await deploy('RewardsPool', [sdlPool.address, token1.address]) + await sdlPool.addToken(token1.address, rewardsPool1.address) + assert.equal((await controller.checkUpkeep('0x'))[0], false) + assert.equal(await controller.shouldUpdate(), false) + + await offRamp + .connect(signers[4]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + '0x', + controller.address, + [{ token: token1.address, amount: toEther(25) }] + ) + + assert.equal((await controller.checkUpkeep('0x'))[0], false) + assert.equal(await controller.shouldUpdate(), false) await sdlToken.transferAndCall( sdlPool.address, toEther(100), ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) ) - assert.equal((await controller.checkUpkeep('0x'))[0], true) - await controller.performUpkeep('0x') assert.equal((await controller.checkUpkeep('0x'))[0], false) + assert.equal(await controller.shouldUpdate(), false) await offRamp .connect(signers[4]) .executeSingleMessage( ethers.utils.formatBytes32String('messageId'), 77, - ethers.utils.defaultAbiCoder.encode(['uint256'], [3]), + '0x', controller.address, - [] + [{ token: token1.address, amount: toEther(25) }] ) - assert.equal((await controller.checkUpkeep('0x'))[0], false) - - await sdlPool.connect(signers[1]).withdraw(2, toEther(10)) - assert.equal((await controller.checkUpkeep('0x'))[0], false) - await time.increase(10000) assert.equal((await controller.checkUpkeep('0x'))[0], true) + assert.equal(await controller.shouldUpdate(), true) + + await controller.performUpkeep('0x') + assert.equal((await controller.checkUpkeep('0x'))[0], false) + assert.equal(await controller.shouldUpdate(), false) }) it('performUpkeep should work correctly', async () => { + await token1.transfer(tokenPool.address, toEther(1000)) + let rewardsPool1 = await deploy('RewardsPool', [sdlPool.address, token1.address]) + await sdlPool.addToken(token1.address, rewardsPool1.address) + await expect(controller.performUpkeep('0x')).to.be.revertedWith('UpdateConditionsNotMet()') await sdlToken.transferAndCall( @@ -178,6 +199,15 @@ describe('SDLPoolCCIPControllerSecondary', () => { toEther(100), ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) ) + await offRamp + .connect(signers[4]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + '0x', + controller.address, + [{ token: token1.address, amount: toEther(25) }] + ) await controller.performUpkeep('0x') await expect(controller.performUpkeep('0x')).to.be.revertedWith('UpdateConditionsNotMet()') @@ -215,7 +245,15 @@ describe('SDLPoolCCIPControllerSecondary', () => { await sdlPool.connect(signers[1]).withdraw(2, toEther(10)) await expect(controller.performUpkeep('0x')).to.be.revertedWith('UpdateConditionsNotMet()') - await time.increase(10000) + await offRamp + .connect(signers[4]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + '0x', + controller.address, + [{ token: token1.address, amount: toEther(25) }] + ) await controller.performUpkeep('0x') lastRequestData = await onRamp.getLastRequestData() @@ -286,6 +324,7 @@ describe('SDLPoolCCIPControllerSecondary', () => { ] ) + assert.equal(await controller.shouldUpdate(), false) assert.equal(fromEther(await token1.balanceOf(rewardsPool1.address)), 30) assert.equal(fromEther(await token2.balanceOf(rewardsPool2.address)), 60) assert.deepEqual( @@ -296,14 +335,47 @@ describe('SDLPoolCCIPControllerSecondary', () => { (await sdlPool.withdrawableRewards(accounts[1])).map((d) => fromEther(d)), [15, 30] ) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) + ) + await offRamp + .connect(signers[4]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + '0x', + controller.address, + [ + { token: token1.address, amount: toEther(30) }, + { token: token2.address, amount: toEther(60) }, + ] + ) + + assert.equal(await controller.shouldUpdate(), true) }) it('ccipReceive should work correctly for incoming updates', async () => { + await token1.transfer(tokenPool.address, toEther(1000)) + let rewardsPool1 = await deploy('RewardsPool', [sdlPool.address, token1.address]) + await sdlPool.addToken(token1.address, rewardsPool1.address) + await sdlToken.transferAndCall( sdlPool.address, toEther(300), ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) ) + await offRamp + .connect(signers[4]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + '0x', + controller.address, + [{ token: token1.address, amount: toEther(30) }] + ) await controller.performUpkeep('0x') let success: any = await offRamp @@ -326,8 +398,9 @@ describe('SDLPoolCCIPControllerSecondary', () => { controller.address, [] ) - await sdlPool.executeQueuedOperations([]) + assert.equal(await controller.shouldUpdate(), false) + await sdlPool.executeQueuedOperations([]) assert.deepEqual(parseLock((await sdlPool.getLocks([7]))[0]), { amount: 300, boostAmount: 0, diff --git a/test/core/ccip/wrapped-token-bridge.test.ts b/test/core/ccip/wrapped-token-bridge.test.ts index 31dd520c..7fa58c6c 100644 --- a/test/core/ccip/wrapped-token-bridge.test.ts +++ b/test/core/ccip/wrapped-token-bridge.test.ts @@ -56,6 +56,7 @@ describe('WrappedTokenBridge', () => { await stakingPool.addStrategy(strategy.address) await stakingPool.setPriorityPool(accounts[0]) + await stakingPool.setRewardsInitiator(accounts[0]) await linkToken.approve(stakingPool.address, ethers.constants.MaxUint256) await stakingPool.deposit(accounts[0], toEther(10000)) @@ -93,16 +94,16 @@ describe('WrappedTokenBridge', () => { }) it('getFee should work correctly', async () => { - assert.equal(fromEther(await bridge.getFee(77, false, '0x')), 2) - assert.equal(fromEther(await bridge.getFee(77, true, '0x')), 3) - await expect(bridge.getFee(78, false, '0x')).to.be.reverted - await expect(bridge.getFee(78, true, '0x')).to.be.reverted + assert.equal(fromEther(await bridge.getFee(77, false)), 2) + assert.equal(fromEther(await bridge.getFee(77, true)), 3) + await expect(bridge.getFee(78, false)).to.be.reverted + await expect(bridge.getFee(78, true)).to.be.reverted }) it('transferTokens should work correctly with LINK fee', async () => { let preFeeBalance = await linkToken.balanceOf(accounts[0]) - await bridge.transferTokens(77, accounts[4], toEther(100), false, toEther(10), '0x') + await bridge.transferTokens(77, accounts[4], toEther(100), false, toEther(10)) let lastRequestData = await onRamp.getLastRequestData() let lastRequestMsg = await onRamp.getLastRequestMessage() @@ -124,14 +125,14 @@ describe('WrappedTokenBridge', () => { assert.equal(lastRequestMsg[3], linkToken.address) await expect( - bridge.transferTokens(77, accounts[4], toEther(100), false, toEther(1), '0x') + bridge.transferTokens(77, accounts[4], toEther(100), false, toEther(1)) ).to.be.revertedWith('FeeExceedsLimit()') }) it('transferTokens should work correctly with native fee', async () => { let preFeeBalance = await ethers.provider.getBalance(accounts[0]) - await bridge.transferTokens(77, accounts[4], toEther(100), true, 0, '0x', { + await bridge.transferTokens(77, accounts[4], toEther(100), true, 0, { value: toEther(10), }) let lastRequestData = await onRamp.getLastRequestData() diff --git a/test/core/priorityPool/priority-pool.test.ts b/test/core/priorityPool/priority-pool.test.ts index 664e2802..fd74a798 100644 --- a/test/core/priorityPool/priority-pool.test.ts +++ b/test/core/priorityPool/priority-pool.test.ts @@ -61,6 +61,7 @@ describe('PriorityPool', () => { await stakingPool.addStrategy(strategy.address) await stakingPool.setPriorityPool(sq.address) + await stakingPool.setRewardsInitiator(accounts[0]) await sq.setDistributionOracle(accounts[0]) for (let i = 0; i < signers.length; i++) { diff --git a/test/core/slashing-keeper.test.ts b/test/core/rewards-initiator.test.ts similarity index 62% rename from test/core/slashing-keeper.test.ts rename to test/core/rewards-initiator.test.ts index 754229ba..cef9bd7e 100644 --- a/test/core/slashing-keeper.test.ts +++ b/test/core/rewards-initiator.test.ts @@ -13,14 +13,16 @@ import { StrategyMock, StakingPool, WrappedSDToken, - SlashingKeeper, + RewardsInitiator, + SDLPoolCCIPControllerMock, } from '../../typechain-types' -describe('SlashingKeeper', () => { +describe('RewardsInitiator', () => { let token: ERC677 let wsdToken: WrappedSDToken - let slashingKeeper: SlashingKeeper + let rewardsInitiator: RewardsInitiator let stakingPool: StakingPool + let sdlPoolCCIPController: SDLPoolCCIPControllerMock let strategy1: StrategyMock let strategy2: StrategyMock let strategy3: StrategyMock @@ -46,7 +48,15 @@ describe('SlashingKeeper', () => { [[ownersRewards, 1000]], ])) as StakingPool - slashingKeeper = (await deploy('SlashingKeeper', [stakingPool.address])) as SlashingKeeper + sdlPoolCCIPController = (await deploy('SDLPoolCCIPControllerMock', [ + accounts[0], + accounts[0], + ])) as SDLPoolCCIPControllerMock + + rewardsInitiator = (await deploy('RewardsInitiator', [ + stakingPool.address, + sdlPoolCCIPController.address, + ])) as RewardsInitiator wsdToken = (await deploy('WrappedSDToken', [ stakingPool.address, @@ -77,6 +87,7 @@ describe('SlashingKeeper', () => { await stakingPool.addStrategy(strategy2.address) await stakingPool.addStrategy(strategy3.address) await stakingPool.setPriorityPool(accounts[0]) + await stakingPool.setRewardsInitiator(rewardsInitiator.address) await token.approve(stakingPool.address, ethers.constants.MaxUint256) await stakingPool.deposit(accounts[0], toEther(1000)) @@ -85,12 +96,12 @@ describe('SlashingKeeper', () => { it('checkUpkeep should work correctly', async () => { await token.transfer(strategy2.address, toEther(100)) - let data = await slashingKeeper.checkUpkeep('0x00') + let data = await rewardsInitiator.checkUpkeep('0x00') assert.equal(data[0], false, 'upkeepNeeded incorrect') await strategy3.simulateSlash(toEther(20)) - data = await slashingKeeper.checkUpkeep('0x00') + data = await rewardsInitiator.checkUpkeep('0x00') assert.equal(data[0], true, 'upkeepNeeded incorrect') assert.deepEqual( decode(data[1])[0].map((v: any) => v.toNumber()), @@ -99,7 +110,7 @@ describe('SlashingKeeper', () => { await strategy1.simulateSlash(toEther(30)) - data = await slashingKeeper.checkUpkeep('0x00') + data = await rewardsInitiator.checkUpkeep('0x00') assert.equal(data[0], true, 'upkeepNeeded incorrect') assert.deepEqual( decode(data[1])[0].map((v: any) => v.toNumber()), @@ -112,9 +123,9 @@ describe('SlashingKeeper', () => { await strategy1.simulateSlash(toEther(10)) await strategy3.simulateSlash(toEther(10)) - await slashingKeeper.performUpkeep(encode([0, 2])) + await rewardsInitiator.performUpkeep(encode([0, 2])) - let data = await slashingKeeper.checkUpkeep('0x00') + let data = await rewardsInitiator.checkUpkeep('0x00') assert.equal(data[0], false, 'upkeepNeeded incorrect') assert.equal( fromEther(await strategy1.getDepositChange()), @@ -134,11 +145,34 @@ describe('SlashingKeeper', () => { await strategy3.simulateSlash(toEther(10)) - await expect(slashingKeeper.performUpkeep(encode([0, 2]))).to.be.revertedWith( - 'Deposit change is >= 0' + await expect(rewardsInitiator.performUpkeep(encode([0, 2]))).to.be.revertedWith( + 'PositiveDepositChange()' ) - await expect(slashingKeeper.performUpkeep(encode([]))).to.be.revertedWith( - 'No strategies to update' + await expect(rewardsInitiator.performUpkeep(encode([]))).to.be.revertedWith( + 'NoStrategiesToUpdate()' ) }) + + it('updateRewards should work correctly', async () => { + await token.transfer(strategy2.address, toEther(100)) + await strategy1.simulateSlash(toEther(10)) + await strategy3.simulateSlash(toEther(10)) + + await rewardsInitiator.updateRewards([0, 2], '0x') + + assert.equal(fromEther(await strategy1.getDepositChange()), 0) + assert.equal(fromEther(await strategy2.getDepositChange()), 100) + assert.equal(fromEther(await strategy3.getDepositChange()), 0) + assert.equal((await sdlPoolCCIPController.rewardsDistributed()).toNumber(), 1) + + await token.transfer(strategy2.address, toEther(10)) + await token.transfer(strategy3.address, toEther(20)) + + await rewardsInitiator.updateRewards([0, 1, 2], '0x') + + assert.equal(fromEther(await strategy1.getDepositChange()), 0) + assert.equal(fromEther(await strategy2.getDepositChange()), 0) + assert.equal(fromEther(await strategy3.getDepositChange()), 0) + assert.equal((await sdlPoolCCIPController.rewardsDistributed()).toNumber(), 2) + }) }) diff --git a/test/core/sdlPool/sdl-pool-primary.test.ts b/test/core/sdlPool/sdl-pool-primary.test.ts index b4d68d5e..9392e421 100644 --- a/test/core/sdlPool/sdl-pool-primary.test.ts +++ b/test/core/sdlPool/sdl-pool-primary.test.ts @@ -1195,13 +1195,25 @@ describe('SDLPoolPrimary', () => { ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) ) await expect( - sdlPool.handleIncomingRESDL(accounts[1], 1, toEther(100), toEther(50), 123, 456, 789) + sdlPool.handleIncomingRESDL(accounts[1], 1, { + amount: toEther(100), + boostAmount: toEther(50), + startTime: 123, + duration: 456, + expiry: 789, + }) ).to.be.revertedWith('InvalidLockId()') await sdlPool.handleOutgoingRESDL(accounts[0], 1, accounts[0]) const startingEffectiveBalance = await sdlPool.totalEffectiveBalance() - await sdlPool.handleIncomingRESDL(accounts[1], 7, toEther(100), toEther(50), 123, 456, 0) + await sdlPool.handleIncomingRESDL(accounts[1], 7, { + amount: toEther(100), + boostAmount: toEther(50), + startTime: 123, + duration: 456, + expiry: 0, + }) assert.deepEqual(parseLocks(await sdlPool.getLocks([7]))[0], { amount: 100, boostAmount: 50, @@ -1215,7 +1227,13 @@ describe('SDLPoolPrimary', () => { assert.equal(await sdlPool.ownerOf(7), accounts[1]) assert.equal((await sdlPool.balanceOf(accounts[1])).toNumber(), 1) - await sdlPool.handleIncomingRESDL(accounts[2], 9, toEther(200), toEther(400), 1, 2, 3) + await sdlPool.handleIncomingRESDL(accounts[2], 9, { + amount: toEther(200), + boostAmount: toEther(400), + startTime: 1, + duration: 2, + expiry: 3, + }) assert.deepEqual(parseLocks(await sdlPool.getLocks([9]))[0], { amount: 200, boostAmount: 400, @@ -1234,7 +1252,13 @@ describe('SDLPoolPrimary', () => { let rewards1 = await rewardsPool.withdrawableRewards(accounts[3]) let rewards2 = await rewardsPool.withdrawableRewards(accounts[0]) - await sdlPool.handleIncomingRESDL(accounts[3], 10, toEther(50), toEther(100), 1, 2, 3) + await sdlPool.handleIncomingRESDL(accounts[3], 10, { + amount: toEther(50), + boostAmount: toEther(100), + startTime: 1, + duration: 2, + expiry: 3, + }) assert.isTrue((await rewardsPool.withdrawableRewards(accounts[3])).eq(rewards1)) assert.isTrue((await rewardsPool.withdrawableRewards(accounts[0])).eq(rewards2)) @@ -1280,4 +1304,18 @@ describe('SDLPoolPrimary', () => { assert.equal(fromEther(await sdlPool.totalEffectiveBalance()), 3600) assert.equal(fromEther(await sdlPool.effectiveBalanceOf(accounts[0])), 1600) }) + + it('should not be able to transfer to ccip controller', async () => { + await sdlToken + .connect(signers[1]) + .transferAndCall( + sdlPool.address, + toEther(1000), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + + await expect( + sdlPool.connect(signers[1]).transferFrom(accounts[1], accounts[0], 1) + ).to.be.revertedWith('TransferToCCIPController()') + }) }) diff --git a/test/core/sdlPool/sdl-pool-secondary.test.ts b/test/core/sdlPool/sdl-pool-secondary.test.ts index fb7d47a7..76bbf891 100644 --- a/test/core/sdlPool/sdl-pool-secondary.test.ts +++ b/test/core/sdlPool/sdl-pool-secondary.test.ts @@ -91,6 +91,7 @@ describe('SDLPoolSecondary', () => { 'reSDL', sdlToken.address, boostController.address, + 5, ])) as SDLPoolSecondary rewardsPool = (await deploy('RewardsPool', [ @@ -1403,10 +1404,22 @@ describe('SDLPoolSecondary', () => { it('handleIncomingRESDL should work correctly', async () => { await mintLock(false) await expect( - sdlPool.handleIncomingRESDL(accounts[1], 1, toEther(100), toEther(50), 123, 456, 789) + sdlPool.handleIncomingRESDL(accounts[1], 1, { + amount: toEther(100), + boostAmount: toEther(50), + startTime: 123, + duration: 456, + expiry: 789, + }) ).to.be.revertedWith('InvalidLockId()') - await sdlPool.handleIncomingRESDL(accounts[1], 7, toEther(100), toEther(50), 123, 456, 0) + await sdlPool.handleIncomingRESDL(accounts[1], 7, { + amount: toEther(100), + boostAmount: toEther(50), + startTime: 123, + duration: 456, + expiry: 0, + }) assert.deepEqual(parseLocks(await sdlPool.getLocks([7]))[0], { amount: 100, boostAmount: 50, @@ -1420,7 +1433,13 @@ describe('SDLPoolSecondary', () => { assert.equal((await sdlPool.balanceOf(accounts[1])).toNumber(), 1) assert.equal((await sdlPool.lastLockId()).toNumber(), 7) - await sdlPool.handleIncomingRESDL(accounts[2], 9, toEther(200), toEther(400), 1, 2, 3) + await sdlPool.handleIncomingRESDL(accounts[2], 9, { + amount: toEther(200), + boostAmount: toEther(400), + startTime: 1, + duration: 2, + expiry: 3, + }) assert.deepEqual(parseLocks(await sdlPool.getLocks([9]))[0], { amount: 200, boostAmount: 400, @@ -1439,7 +1458,13 @@ describe('SDLPoolSecondary', () => { let rewards1 = await rewardsPool.withdrawableRewards(accounts[3]) let rewards2 = await rewardsPool.withdrawableRewards(accounts[0]) - await sdlPool.handleIncomingRESDL(accounts[3], 10, toEther(50), toEther(100), 1, 2, 3) + await sdlPool.handleIncomingRESDL(accounts[3], 10, { + amount: toEther(50), + boostAmount: toEther(100), + startTime: 1, + duration: 2, + expiry: 3, + }) assert.isTrue((await rewardsPool.withdrawableRewards(accounts[3])).eq(rewards1)) assert.isTrue((await rewardsPool.withdrawableRewards(accounts[0])).eq(rewards2)) @@ -1868,4 +1893,48 @@ describe('SDLPoolSecondary', () => { { amount: 600, boostAmount: 0, startTime: 0, duration: 0, expiry: 0 }, ]) }) + + it('should not be able to queue more locks than the limit', async () => { + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await expect( + sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + ).to.be.revertedWith('TooManyQueuedLocks()') + + await updateLocks(1, []) + await sdlPool.executeQueuedOperations([]) + + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [1, 0]) + ) + }) }) diff --git a/test/core/staking-pool.test.ts b/test/core/staking-pool.test.ts index 29574372..2902c4c6 100644 --- a/test/core/staking-pool.test.ts +++ b/test/core/staking-pool.test.ts @@ -76,6 +76,7 @@ describe('StakingPool', () => { await stakingPool.addStrategy(strategy2.address) await stakingPool.addStrategy(strategy3.address) await stakingPool.setPriorityPool(accounts[0]) + await stakingPool.setRewardsInitiator(accounts[0]) await token.approve(stakingPool.address, ethers.constants.MaxUint256) }) diff --git a/test/core/wrapped-sd-token.test.ts b/test/core/wrapped-sd-token.test.ts index 18ff5f50..19fad506 100644 --- a/test/core/wrapped-sd-token.test.ts +++ b/test/core/wrapped-sd-token.test.ts @@ -56,6 +56,7 @@ describe('WrappedSDToken', () => { await stakingPool.addStrategy(strategy1.address) await stakingPool.setPriorityPool(accounts[0]) + await stakingPool.setRewardsInitiator(accounts[0]) await token.approve(stakingPool.address, ethers.constants.MaxUint256) }) diff --git a/test/ethStaking/eth-staking-strategy.test.ts b/test/ethStaking/eth-staking-strategy.test.ts index d9c3fdea..637bb995 100644 --- a/test/ethStaking/eth-staking-strategy.test.ts +++ b/test/ethStaking/eth-staking-strategy.test.ts @@ -171,6 +171,7 @@ describe('EthStakingStrategy', () => { await strategy.setRewardsReceiver(rewardsReceiver.address) await stakingPool.addStrategy(strategy.address) await stakingPool.setPriorityPool(accounts[0]) + await stakingPool.setRewardsInitiator(accounts[0]) await wETH.approve(stakingPool.address, ethers.constants.MaxUint256) }) diff --git a/test/linkStaking/operator-vcs-1.5.test.ts b/test/linkStaking/operator-vcs-1.5.test.ts deleted file mode 100644 index f294d33e..00000000 --- a/test/linkStaking/operator-vcs-1.5.test.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { ethers } from 'hardhat' -import { assert } from 'chai' -import { - toEther, - deploy, - deployUpgradeable, - getAccounts, - fromEther, - deployImplementation, -} from '../utils/helpers' -import { ERC677, StakingPool, StakingMockV1, OperatorVCSUpgrade } from '../../typechain-types' - -describe('OperatorVCSUpgrade', () => { - let token: ERC677 - let staking: StakingMockV1 - let strategy: OperatorVCSUpgrade - let stakingPool: StakingPool - let vaults: string[] - let accounts: string[] - - const encode = (data: any) => ethers.utils.defaultAbiCoder.encode(['uint'], [data]) - - before(async () => { - ;({ accounts } = await getAccounts()) - }) - - beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 - - staking = (await deploy('StakingMockV1', [token.address])) as StakingMockV1 - let vaultImplementation = await deployImplementation('OperatorVaultV1') - - stakingPool = (await deployUpgradeable('StakingPool', [ - token.address, - 'Staked LINK', - 'stLINK', - [], - ])) as StakingPool - - strategy = (await deployUpgradeable('OperatorVCSUpgrade', [ - token.address, - stakingPool.address, - staking.address, - vaultImplementation, - toEther(1000), - [[accounts[4], 500]], - [], - ])) as OperatorVCSUpgrade - - await stakingPool.addStrategy(strategy.address) - await stakingPool.setPriorityPool(accounts[0]) - - for (let i = 0; i < 10; i++) { - await strategy.addVault(accounts[0]) - } - - vaults = await strategy.getVaults() - - await token.approve(stakingPool.address, ethers.constants.MaxUint256) - }) - - it('should be able to add vault', async () => { - await strategy.addVault(accounts[1]) - let vault = await ethers.getContractAt('OperatorVault', (await strategy.getVaults())[10]) - assert.equal(await vault.token(), token.address) - assert.equal(await vault.stakeController(), staking.address) - assert.equal(await vault.vaultController(), strategy.address) - assert.equal(await vault.operator(), accounts[1]) - }) - - it('should be able to get vault deposit limits', async () => { - assert.deepEqual( - (await strategy.getVaultDepositLimits()).map((v) => fromEther(v)), - [10, 50000] - ) - }) - - it('depositBufferedTokens should work correctly', async () => { - await stakingPool.deposit(accounts[0], toEther(1000)) - await strategy.performUpkeep(encode(0)) - assert.equal(fromEther(await staking.getStake(vaults[0])), 1000) - - await stakingPool.deposit(accounts[0], toEther(50000)) - await strategy.performUpkeep(encode(0)) - assert.equal(fromEther(await staking.getStake(vaults[0])), 50000) - assert.equal(fromEther(await staking.getStake(vaults[1])), 1000) - - await stakingPool.deposit(accounts[0], toEther(99009)) - await strategy.performUpkeep(encode(1)) - assert.equal(fromEther(await staking.getStake(vaults[1])), 50000) - assert.equal(fromEther(await staking.getStake(vaults[2])), 50000) - assert.equal(fromEther(await staking.getStake(vaults[3])), 0) - - assert.equal(fromEther(await strategy.getTotalDeposits()), 150009) - }) - - it('getMinDeposits should work correctly', async () => { - await stakingPool.deposit(accounts[0], toEther(1000)) - token.transfer(strategy.address, toEther(100)) - assert.equal(fromEther(await strategy.getMinDeposits()), 1000) - - await strategy.performUpkeep(encode(0)) - assert.equal(fromEther(await strategy.getMinDeposits()), 1000) - - await stakingPool.deposit(accounts[0], toEther(50000)) - assert.equal(fromEther(await strategy.getMinDeposits()), 51000) - - await staking.setBaseReward(toEther(10)) - assert.equal(fromEther(await strategy.getMinDeposits()), 51000) - await stakingPool.updateStrategyRewards([0], '0x') - assert.equal(fromEther(await strategy.getMinDeposits()), 51200) - - await staking.setDelegationReward(toEther(5)) - assert.equal(fromEther(await strategy.getMinDeposits()), 51200) - await stakingPool.updateStrategyRewards([0], '0x') - assert.equal(fromEther(await strategy.getMinDeposits()), 51250) - }) - - it('getMaxDeposits should work correctly', async () => { - await stakingPool.deposit(accounts[0], toEther(1000)) - token.transfer(strategy.address, toEther(100)) - assert.equal(fromEther(await strategy.getMaxDeposits()), 500000) - - await strategy.performUpkeep(encode(0)) - assert.equal(fromEther(await strategy.getMaxDeposits()), 500000) - - await stakingPool.deposit(accounts[0], toEther(50000)) - assert.equal(fromEther(await strategy.getMaxDeposits()), 500000) - - await staking.setBaseReward(toEther(10)) - assert.equal(fromEther(await strategy.getMaxDeposits()), 500000) - await stakingPool.updateStrategyRewards([0], '0x') - assert.equal(fromEther(await strategy.getMaxDeposits()), 500200) - - await staking.setDelegationReward(toEther(5)) - assert.equal(fromEther(await strategy.getMaxDeposits()), 500200) - await stakingPool.updateStrategyRewards([0], '0x') - assert.equal(fromEther(await strategy.getMaxDeposits()), 500250) - }) - - it('getStrategyRewards should work correctly', async () => { - await stakingPool.deposit(accounts[0], toEther(55000)) - await strategy.depositBufferedTokens(0) - - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [0, 0] - ) - - await staking.setBaseReward(toEther(10)) - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [100, 5] - ) - - await staking.setDelegationReward(toEther(5)) - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [150, 7.5] - ) - - await token.transfer(strategy.address, toEther(50)) - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [200, 10] - ) - }) - - it('getStrategyRewards should work correctly with slashing', async () => { - await stakingPool.deposit(accounts[0], toEther(55000)) - await strategy.depositBufferedTokens(0) - await staking.setBaseReward(toEther(10)) - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [100, 5] - ) - - await staking.setBaseReward(toEther(5)) - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [50, 2.5] - ) - - await stakingPool.updateStrategyRewards([0], '0x') - await staking.setBaseReward(toEther(0)) - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [-50, 0] - ) - }) - - it('updateStrategyRewards should work correctly', async () => { - await stakingPool.deposit(accounts[0], toEther(400)) - await strategy.depositBufferedTokens(0) - - await stakingPool.updateStrategyRewards([0], '0x') - assert.equal(fromEther(await strategy.getTotalDeposits()), 400) - assert.equal(fromEther(await stakingPool.totalStaked()), 400) - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [0, 0] - ) - - await staking.setBaseReward(toEther(10)) - await stakingPool.updateStrategyRewards([0], '0x') - assert.equal(fromEther(await strategy.getTotalDeposits()), 500) - assert.equal(fromEther(await stakingPool.totalStaked()), 500) - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [0, 0] - ) - await staking.setDelegationReward(toEther(5)) - await stakingPool.updateStrategyRewards([0], '0x') - assert.equal(fromEther(await strategy.getTotalDeposits()), 550) - assert.equal(fromEther(await stakingPool.totalStaked()), 550) - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [0, 0] - ) - await token.transfer(strategy.address, toEther(20)) - await stakingPool.updateStrategyRewards([0], '0x') - assert.equal(fromEther(await strategy.getTotalDeposits()), 570) - assert.equal(fromEther(await stakingPool.totalStaked()), 570) - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [0, 0] - ) - }) - - it('updateStrategyRewards should work correctly with slashing', async () => { - await stakingPool.deposit(accounts[0], toEther(400)) - await strategy.depositBufferedTokens(0) - await staking.setBaseReward(toEther(10)) - await stakingPool.updateStrategyRewards([0], '0x') - assert.equal(fromEther(await strategy.getTotalDeposits()), 500) - assert.equal(fromEther(await stakingPool.totalStaked()), 500) - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [0, 0] - ) - await staking.setBaseReward(toEther(5)) - await stakingPool.updateStrategyRewards([0], '0x') - assert.equal(fromEther(await strategy.getTotalDeposits()), 450) - assert.equal(fromEther(await stakingPool.totalStaked()), 450) - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [0, 0] - ) - await staking.setBaseReward(toEther(0)) - await stakingPool.updateStrategyRewards([0], '0x') - assert.equal(fromEther(await strategy.getTotalDeposits()), 400) - assert.equal(fromEther(await stakingPool.totalStaked()), 400) - assert.deepEqual( - (await stakingPool.getStrategyRewards([0])).map((v) => fromEther(v)), - [0, 0] - ) - }) - - it('fees should be properly calculated in updateStrategyRewards', async () => { - await stakingPool.deposit(accounts[0], toEther(400)) - await strategy.depositBufferedTokens(0) - - await staking.setBaseReward(toEther(10)) - await strategy.addFee(accounts[3], 1000) - await stakingPool.updateStrategyRewards([0], '0x') - - assert.equal(fromEther(await stakingPool.balanceOf(accounts[4])), 5) - assert.equal(fromEther(await stakingPool.balanceOf(accounts[3])), 10) - - await staking.setBaseReward(toEther(0)) - await stakingPool.updateStrategyRewards([0], '0x') - - assert.equal(fromEther(await stakingPool.balanceOf(accounts[4])), 4) - assert.equal(fromEther(await stakingPool.balanceOf(accounts[3])), 8) - }) -}) diff --git a/test/linkStaking/operator-vcs.test.ts b/test/linkStaking/operator-vcs.test.ts index 44892358..6a8c317c 100644 --- a/test/linkStaking/operator-vcs.test.ts +++ b/test/linkStaking/operator-vcs.test.ts @@ -70,6 +70,7 @@ describe('OperatorVCS', () => { await stakingPool.addStrategy(strategy.address) await stakingPool.setPriorityPool(accounts[0]) + await stakingPool.setRewardsInitiator(accounts[0]) for (let i = 0; i < 15; i++) { await strategy.addVault(accounts[0], accounts[1], pfAlertsController.address) From f13971a98efda7dd0591efa8c67dd2ce643ec75c Mon Sep 17 00:00:00 2001 From: BkChoy Date: Tue, 19 Dec 2023 09:47:02 -0500 Subject: [PATCH 27/42] added caller whitelist to rewards initiator --- contracts/core/RewardsInitiator.sol | 22 ++++++++++++++++++++-- test/core/rewards-initiator.test.ts | 1 + 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/contracts/core/RewardsInitiator.sol b/contracts/core/RewardsInitiator.sol index 9994bf7c..13e181b3 100644 --- a/contracts/core/RewardsInitiator.sol +++ b/contracts/core/RewardsInitiator.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.15; +import "@openzeppelin/contracts/access/Ownable.sol"; + import "./interfaces/IStakingPool.sol"; import "./interfaces/IStrategy.sol"; import "./interfaces/ISDLPoolCCIPControllerPrimary.sol"; @@ -11,12 +13,17 @@ import "./interfaces/ISDLPoolCCIPControllerPrimary.sol"; * @dev Chainlink automation should call updateRewards periodically under normal circumstances and call performUpkeep * in the case of a negative rebase in the staking pool */ -contract RewardsInitiator { +contract RewardsInitiator is Ownable { IStakingPool public stakingPool; ISDLPoolCCIPControllerPrimary public sdlPoolCCIPController; + mapping(address => bool) public whitelistedCallers; + + event WhitelistCaller(address indexed caller, bool shouldWhitelist); + error NoStrategiesToUpdate(); error PositiveDepositChange(); + error SenderNotAuthorized(); constructor(address _stakingPool, address _sdlPoolCCIPController) { stakingPool = IStakingPool(_stakingPool); @@ -29,12 +36,13 @@ contract RewardsInitiator { * @param _data encoded data to be passed to each strategy **/ function updateRewards(uint256[] calldata _strategyIdxs, bytes calldata _data) external { + if (!whitelistedCallers[msg.sender]) revert SenderNotAuthorized(); stakingPool.updateStrategyRewards(_strategyIdxs, _data); sdlPoolCCIPController.distributeRewards(); } /** - * @notice returns whether or not rewards should be updated due to a neagtive rebase and the strategies to update + * @notice returns whether or not rewards should be updated due to a negative rebase and the strategies to update * @return upkeepNeeded whether or not rewards should be updated * @return performData abi encoded list of strategy indexes to update **/ @@ -84,4 +92,14 @@ contract RewardsInitiator { stakingPool.updateStrategyRewards(strategiesToUpdate, ""); } + + /** + * @notice Adds or removes an address from the whitelist for calling updateRewards + * @param _caller address to add/remove + * @param _shouldWhitelist whether address should be whitelisted + */ + function whitelistCaller(address _caller, bool _shouldWhitelist) external onlyOwner { + whitelistedCallers[_caller] = _shouldWhitelist; + emit WhitelistCaller(_caller, _shouldWhitelist); + } } diff --git a/test/core/rewards-initiator.test.ts b/test/core/rewards-initiator.test.ts index cef9bd7e..7703e0c2 100644 --- a/test/core/rewards-initiator.test.ts +++ b/test/core/rewards-initiator.test.ts @@ -88,6 +88,7 @@ describe('RewardsInitiator', () => { await stakingPool.addStrategy(strategy3.address) await stakingPool.setPriorityPool(accounts[0]) await stakingPool.setRewardsInitiator(rewardsInitiator.address) + await rewardsInitiator.whitelistCaller(accounts[0], true) await token.approve(stakingPool.address, ethers.constants.MaxUint256) await stakingPool.deposit(accounts[0], toEther(1000)) From cf6e3d817c5ed52b057e2aff568f4fa942c01808 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 15 Jan 2024 09:37:25 +1300 Subject: [PATCH 28/42] script to deploy ccip dest tokens --- .../test/chainlink/CLContractImports0.8.sol | 1 + scripts/prod/deploy-ccip-dest-tokens.ts | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 scripts/prod/deploy-ccip-dest-tokens.ts diff --git a/contracts/core/test/chainlink/CLContractImports0.8.sol b/contracts/core/test/chainlink/CLContractImports0.8.sol index 6bc07448..863d5457 100644 --- a/contracts/core/test/chainlink/CLContractImports0.8.sol +++ b/contracts/core/test/chainlink/CLContractImports0.8.sol @@ -2,3 +2,4 @@ pragma solidity ^0.8.0; import {Router} from "@chainlink/contracts-ccip/src/v0.8/ccip/Router.sol"; +import {BurnMintERC677} from "@chainlink/contracts/src/v0.8/shared/token/ERC677/BurnMintERC677.sol"; diff --git a/scripts/prod/deploy-ccip-dest-tokens.ts b/scripts/prod/deploy-ccip-dest-tokens.ts new file mode 100644 index 00000000..d4c51d63 --- /dev/null +++ b/scripts/prod/deploy-ccip-dest-tokens.ts @@ -0,0 +1,42 @@ +import { updateDeployments, deploy } from '../utils/deployment' + +// SDL +const sdl = { + name: 'stake.link', + symbol: 'SDL', + decimals: 18, +} +// wstLINK +const wstLINK = { + name: 'Wrapped stLINK', + symbol: 'wstLINK', + decimals: 18, +} + +async function main() { + const sdlToken = await deploy('BurnMintERC677', [sdl.name, sdl.symbol, sdl.decimals, 0]) + console.log('SDLToken deployed: ', sdlToken.address) + + const wrappedSDToken = await deploy('BurnMintERC677', [ + wstLINK.name, + wstLINK.symbol, + wstLINK.decimals, + 0, + ]) + console.log('LINK_WrappedSDToken deployed: ', wrappedSDToken.address) + + updateDeployments( + { + SDLToken: sdlToken.address, + LINK_WrappedSDToken: wrappedSDToken.address, + }, + { SDLToken: 'BurnMintERC677', LINK_wrappedSDToken: 'BurnMintERC677' } + ) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) From 7f57ec1cd5f66b76d1f7586cdd232654c238d581 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Sun, 28 Jan 2024 20:53:42 +1300 Subject: [PATCH 29/42] specify erc677 location --- scripts/test/deploy-test-contracts.ts | 36 +++++++++++++++---- scripts/test/gas.ts | 6 +++- scripts/test/setup-testnet.ts | 6 +++- test/airdrop/merkle-distributor.test.ts | 18 ++++++++-- test/core/ccip/resdl-token-bridge.test.ts | 18 ++++++++-- .../sdl-pool-ccip-controller-primary.test.ts | 24 ++++++++++--- ...sdl-pool-ccip-controller-secondary.test.ts | 24 ++++++++++--- test/core/ccip/wrapped-token-bridge.test.ts | 12 +++++-- test/core/lpl-migration.test.ts | 6 +++- .../priorityPool/distribution-oracle.test.ts | 6 +++- test/core/priorityPool/priority-pool.test.ts | 6 +++- test/core/rewards-initiator.test.ts | 6 +++- test/core/rewards-pool-controller.test.ts | 36 +++++++++++++++---- test/core/sdlPool/sdl-pool-primary.test.ts | 6 +++- test/core/sdlPool/sdl-pool-secondary.test.ts | 6 +++- test/core/staking-pool.test.ts | 6 +++- test/core/strategy.test.ts | 6 +++- test/core/wrapped-sd-token.test.ts | 6 +++- test/ethStaking/key-validation-oracle.test.ts | 12 +++++-- .../nwl-operator-controller.test.ts | 6 +++- test/ethStaking/operator-controller.test.ts | 6 +++- .../ethStaking/wl-operator-controller.test.ts | 6 +++- test/linkStaking/community-vault.test.ts | 6 +++- test/linkStaking/community-vcs.test.ts | 6 +++- test/linkStaking/operator-vault.test.ts | 6 +++- test/linkStaking/operator-vcs.test.ts | 6 +++- .../vault-controller-strategy.test.ts | 6 +++- test/linkStaking/vault.test.ts | 6 +++- test/liquidSDIndex/liquid-sd-adapter.test.ts | 6 +++- .../liquid-sd-index-pool.test.ts | 24 ++++++++++--- 30 files changed, 275 insertions(+), 55 deletions(-) diff --git a/scripts/test/deploy-test-contracts.ts b/scripts/test/deploy-test-contracts.ts index ce3d2658..6369d0a0 100644 --- a/scripts/test/deploy-test-contracts.ts +++ b/scripts/test/deploy-test-contracts.ts @@ -6,10 +6,18 @@ async function main() { throw Error('Test contracts can only be deployed on test networks') } - const lplToken = await deploy('ERC677', ['LinkPool', 'LPL', 100000000]) + const lplToken = await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'LinkPool', + 'LPL', + 100000000, + ]) console.log('LPLToken deployed: ', lplToken.address) - const linkToken = await deploy('ERC677', ['Chainlink', 'LINK', 1000000000]) + const linkToken = await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ]) console.log('LINKToken deployed: ', linkToken.address) const multicall = await deploy('Multicall3', []) @@ -40,10 +48,26 @@ async function main() { ) await tx.wait() - const stETHToken = await deploy('ERC677', ['Lido stETH', 'stETH', 1000000000]) - const rETHToken = await deploy('ERC677', ['RocketPool rETH', 'rETH', 1000000000]) - const cbETHToken = await deploy('ERC677', ['Coinbase cbETH', 'cbETH', 1000000000]) - const sfrxETHToken = await deploy('ERC677', ['Frax sfrxETH', 'sfrxETH', 1000000000]) + const stETHToken = await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Lido stETH', + 'stETH', + 1000000000, + ]) + const rETHToken = await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'RocketPool rETH', + 'rETH', + 1000000000, + ]) + const cbETHToken = await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Coinbase cbETH', + 'cbETH', + 1000000000, + ]) + const sfrxETHToken = await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Frax sfrxETH', + 'sfrxETH', + 1000000000, + ]) updateDeployments( { diff --git a/scripts/test/gas.ts b/scripts/test/gas.ts index b524240d..9c10571c 100644 --- a/scripts/test/gas.ts +++ b/scripts/test/gas.ts @@ -31,7 +31,11 @@ const LINK_CommunityVCS = { async function main() { const { accounts } = await getAccounts() - const linkToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + const linkToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 const stakingPool = (await deployUpgradeable('StakingPool', [ linkToken.address, diff --git a/scripts/test/setup-testnet.ts b/scripts/test/setup-testnet.ts index 5e3a6647..11b92e6f 100644 --- a/scripts/test/setup-testnet.ts +++ b/scripts/test/setup-testnet.ts @@ -59,7 +59,11 @@ async function main() { ]) console.log('SDLPool deployed: ', sdlPool.address) - const linkToken = await deploy('ERC677', ['Chainlink-Test', 'LINK-TEST', 200000000]) + const linkToken = await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink-Test', + 'LINK-TEST', + 200000000, + ]) console.log('LINKToken-TEST deployed: ', linkToken.address) const stakingPool = await deployUpgradeable('StakingPool', [ diff --git a/test/airdrop/merkle-distributor.test.ts b/test/airdrop/merkle-distributor.test.ts index 27613bf5..38b54f5b 100644 --- a/test/airdrop/merkle-distributor.test.ts +++ b/test/airdrop/merkle-distributor.test.ts @@ -26,7 +26,11 @@ describe('MerkleDistributor', () => { wallet0 = accounts[1] wallet1 = accounts[2] - token = (await deploy('ERC677', ['Token', 'TKN', 1000000])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Token', + 'TKN', + 1000000, + ])) as ERC677 }) describe('#claim', () => { @@ -209,8 +213,16 @@ describe('MerkleDistributor', () => { let token2: ERC677 let token3: ERC677 beforeEach('deploy', async () => { - token2 = (await deploy('ERC677', ['Token', 'TKN', 1000000])) as ERC677 - token3 = (await deploy('ERC677', ['Token', 'TKN', 1000000])) as ERC677 + token2 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Token', + 'TKN', + 1000000, + ])) as ERC677 + token3 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Token', + 'TKN', + 1000000, + ])) as ERC677 tree = new BalanceTree([ { account: wallet0, amount: BigNumber.from(100) }, { account: wallet1, amount: BigNumber.from(101) }, diff --git a/test/core/ccip/resdl-token-bridge.test.ts b/test/core/ccip/resdl-token-bridge.test.ts index cf37d654..1abf4999 100644 --- a/test/core/ccip/resdl-token-bridge.test.ts +++ b/test/core/ccip/resdl-token-bridge.test.ts @@ -34,9 +34,21 @@ describe('RESDLTokenBridge', () => { }) beforeEach(async () => { - linkToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 - sdlToken = (await deploy('ERC677', ['SDL', 'SDL', 1000000000])) as ERC677 - token2 = (await deploy('ERC677', ['2', '2', 1000000000])) as ERC677 + linkToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 + sdlToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'SDL', + 'SDL', + 1000000000, + ])) as ERC677 + token2 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + '2', + '2', + 1000000000, + ])) as ERC677 wrappedNative = (await deploy('WrappedNative')) as WrappedNative const armProxy = await deploy('CCIPArmProxyMock') diff --git a/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts b/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts index 4164f611..b84bdd4c 100644 --- a/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts +++ b/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts @@ -40,10 +40,26 @@ describe('SDLPoolCCIPControllerPrimary', () => { }) beforeEach(async () => { - linkToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 - sdlToken = (await deploy('ERC677', ['SDL', 'SDL', 1000000000])) as ERC677 - token1 = (await deploy('ERC677', ['2', '2', 1000000000])) as ERC677 - token2 = (await deploy('ERC677', ['2', '2', 1000000000])) as ERC677 + linkToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 + sdlToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'SDL', + 'SDL', + 1000000000, + ])) as ERC677 + token1 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + '2', + '2', + 1000000000, + ])) as ERC677 + token2 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + '2', + '2', + 1000000000, + ])) as ERC677 const armProxy = await deploy('CCIPArmProxyMock') router = (await deploy('Router', [accounts[0], armProxy.address])) as Router diff --git a/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts index 5136e8dc..515f03b5 100644 --- a/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts +++ b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts @@ -39,10 +39,26 @@ describe('SDLPoolCCIPControllerSecondary', () => { }) beforeEach(async () => { - linkToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 - sdlToken = (await deploy('ERC677', ['SDL', 'SDL', 1000000000])) as ERC677 - token1 = (await deploy('ERC677', ['2', '2', 1000000000])) as ERC677 - token2 = (await deploy('ERC677', ['2', '2', 1000000000])) as ERC677 + linkToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 + sdlToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'SDL', + 'SDL', + 1000000000, + ])) as ERC677 + token1 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + '2', + '2', + 1000000000, + ])) as ERC677 + token2 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + '2', + '2', + 1000000000, + ])) as ERC677 const armProxy = await deploy('CCIPArmProxyMock') const router = await deploy('Router', [accounts[0], armProxy.address]) diff --git a/test/core/ccip/wrapped-token-bridge.test.ts b/test/core/ccip/wrapped-token-bridge.test.ts index 7fa58c6c..aa9f1976 100644 --- a/test/core/ccip/wrapped-token-bridge.test.ts +++ b/test/core/ccip/wrapped-token-bridge.test.ts @@ -31,8 +31,16 @@ describe('WrappedTokenBridge', () => { }) beforeEach(async () => { - linkToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 - token2 = (await deploy('ERC677', ['2', '2', 1000000000])) as ERC677 + linkToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 + token2 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + '2', + '2', + 1000000000, + ])) as ERC677 stakingPool = (await deployUpgradeable('StakingPool', [ linkToken.address, diff --git a/test/core/lpl-migration.test.ts b/test/core/lpl-migration.test.ts index e6c4dfa7..32db458b 100644 --- a/test/core/lpl-migration.test.ts +++ b/test/core/lpl-migration.test.ts @@ -15,7 +15,11 @@ describe('LPLMigration', () => { }) beforeEach(async () => { - lplToken = (await deploy('ERC677', ['LinkPool', 'LPL', 100000000])) as ERC677 + lplToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'LinkPool', + 'LPL', + 100000000, + ])) as ERC677 await setupToken(lplToken, accounts) sdlToken = (await deploy('StakingAllowance', ['Stake Dot Link', 'SDL'])) as StakingAllowance diff --git a/test/core/priorityPool/distribution-oracle.test.ts b/test/core/priorityPool/distribution-oracle.test.ts index 865a56f0..6bfa62c6 100644 --- a/test/core/priorityPool/distribution-oracle.test.ts +++ b/test/core/priorityPool/distribution-oracle.test.ts @@ -17,7 +17,11 @@ describe('DistributionOracle', () => { }) beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 pp = (await deploy('PriorityPoolMock', [toEther(1000)])) as PriorityPoolMock opContract = (await deploy('Operator', [token.address, accounts[0]])) as Operator oracle = (await deploy('DistributionOracle', [ diff --git a/test/core/priorityPool/priority-pool.test.ts b/test/core/priorityPool/priority-pool.test.ts index fd74a798..5ecb1c98 100644 --- a/test/core/priorityPool/priority-pool.test.ts +++ b/test/core/priorityPool/priority-pool.test.ts @@ -32,7 +32,11 @@ describe('PriorityPool', () => { }) beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 await setupToken(token, accounts, true) stakingPool = (await deployUpgradeable('StakingPool', [ diff --git a/test/core/rewards-initiator.test.ts b/test/core/rewards-initiator.test.ts index 7703e0c2..d4bbeefb 100644 --- a/test/core/rewards-initiator.test.ts +++ b/test/core/rewards-initiator.test.ts @@ -38,7 +38,11 @@ describe('RewardsInitiator', () => { }) beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 await setupToken(token, accounts) stakingPool = (await deployUpgradeable('StakingPool', [ diff --git a/test/core/rewards-pool-controller.test.ts b/test/core/rewards-pool-controller.test.ts index 881a93e7..18c31023 100644 --- a/test/core/rewards-pool-controller.test.ts +++ b/test/core/rewards-pool-controller.test.ts @@ -41,11 +41,23 @@ describe('RewardsPoolController', () => { }) beforeEach(async () => { - token1 = (await deploy('ERC677', ['Token1', '1', 1000000000])) as ERC677 + token1 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Token1', + '1', + 1000000000, + ])) as ERC677 await setupToken(token1, accounts) - token2 = (await deploy('ERC677', ['Token2', '2', 1000000000])) as ERC677 + token2 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Token2', + '2', + 1000000000, + ])) as ERC677 await setupToken(token2, accounts) - stakingToken = (await deploy('ERC677', ['StakingToken', 'ST', 1000000000])) as ERC677 + stakingToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'StakingToken', + 'ST', + 1000000000, + ])) as ERC677 await setupToken(stakingToken, accounts) controller = (await deployUpgradeable('RewardsPoolControllerMock', [ @@ -69,7 +81,11 @@ describe('RewardsPoolController', () => { }) it('should be able to add tokens', async () => { - const token3 = (await deploy('ERC677', ['Token3', '3', 1000000000])) as ERC677 + const token3 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Token3', + '3', + 1000000000, + ])) as ERC677 const rewardsPool3 = (await deploy('RewardsPool', [ controller.address, token3.address, @@ -255,9 +271,17 @@ describe('RewardsPoolController', () => { let rewardsPool3: RewardsPoolWSD let rewardsPool4: RewardsPoolWSD beforeEach(async () => { - token3 = (await deploy('ERC677', ['Token3', '3', 1000000000])) as ERC677 + token3 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Token3', + '3', + 1000000000, + ])) as ERC677 await setupToken(token3, accounts) - token4 = (await deploy('ERC677', ['Token4', '4', 1000000000])) as ERC677 + token4 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Token4', + '4', + 1000000000, + ])) as ERC677 await setupToken(token4, accounts) wToken3 = (await deploy('WrappedSDTokenMock', [token3.address])) as WrappedSDTokenMock diff --git a/test/core/sdlPool/sdl-pool-primary.test.ts b/test/core/sdlPool/sdl-pool-primary.test.ts index 9392e421..45d664b2 100644 --- a/test/core/sdlPool/sdl-pool-primary.test.ts +++ b/test/core/sdlPool/sdl-pool-primary.test.ts @@ -44,7 +44,11 @@ describe('SDLPoolPrimary', () => { beforeEach(async () => { sdlToken = (await deploy('StakingAllowance', ['stake.link', 'SDL'])) as StakingAllowance - rewardToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + rewardToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 await sdlToken.mint(accounts[0], toEther(1000000)) await setupToken(sdlToken, accounts) diff --git a/test/core/sdlPool/sdl-pool-secondary.test.ts b/test/core/sdlPool/sdl-pool-secondary.test.ts index 76bbf891..4fc4c3f1 100644 --- a/test/core/sdlPool/sdl-pool-secondary.test.ts +++ b/test/core/sdlPool/sdl-pool-secondary.test.ts @@ -76,7 +76,11 @@ describe('SDLPoolSecondary', () => { beforeEach(async () => { sdlToken = (await deploy('StakingAllowance', ['stake.link', 'SDL'])) as StakingAllowance - rewardToken = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + rewardToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 await sdlToken.mint(accounts[0], toEther(1000000)) await setupToken(sdlToken, accounts) diff --git a/test/core/staking-pool.test.ts b/test/core/staking-pool.test.ts index 2902c4c6..ef56bc36 100644 --- a/test/core/staking-pool.test.ts +++ b/test/core/staking-pool.test.ts @@ -38,7 +38,11 @@ describe('StakingPool', () => { }) beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 await setupToken(token, accounts) erc677Receiver = (await deploy('ERC677ReceiverMock')) as ERC677ReceiverMock diff --git a/test/core/strategy.test.ts b/test/core/strategy.test.ts index d9511f5e..9ef53898 100644 --- a/test/core/strategy.test.ts +++ b/test/core/strategy.test.ts @@ -23,7 +23,11 @@ describe('Strategy', () => { }) beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 await setupToken(token, accounts) strategy = (await deployUpgradeable('StrategyMock', [ diff --git a/test/core/wrapped-sd-token.test.ts b/test/core/wrapped-sd-token.test.ts index 19fad506..3ac020df 100644 --- a/test/core/wrapped-sd-token.test.ts +++ b/test/core/wrapped-sd-token.test.ts @@ -31,7 +31,11 @@ describe('WrappedSDToken', () => { }) beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 await setupToken(token, accounts) stakingPool = (await deployUpgradeable('StakingPool', [ diff --git a/test/ethStaking/key-validation-oracle.test.ts b/test/ethStaking/key-validation-oracle.test.ts index 8f2c5435..880275db 100644 --- a/test/ethStaking/key-validation-oracle.test.ts +++ b/test/ethStaking/key-validation-oracle.test.ts @@ -30,8 +30,16 @@ describe('KeyValidationOracle', () => { }) beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 - let wsdToken = (await deploy('ERC677', ['test', 'test', 0])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 + let wsdToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'test', + 'test', + 0, + ])) as ERC677 nwlOpController = (await deployUpgradeable('OperatorControllerMock', [ accounts[0], wsdToken.address, diff --git a/test/ethStaking/nwl-operator-controller.test.ts b/test/ethStaking/nwl-operator-controller.test.ts index 4ec3dc18..94778f61 100644 --- a/test/ethStaking/nwl-operator-controller.test.ts +++ b/test/ethStaking/nwl-operator-controller.test.ts @@ -59,7 +59,11 @@ describe('NWLOperatorController', () => { }) beforeEach(async () => { - wsdToken = (await deploy('ERC677', ['test', 'test', 100000])) as ERC677 + wsdToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'test', + 'test', + 100000, + ])) as ERC677 controller = (await deployUpgradeable('NWLOperatorController', [ accounts[0], wsdToken.address, diff --git a/test/ethStaking/operator-controller.test.ts b/test/ethStaking/operator-controller.test.ts index 710f49d5..9129c897 100644 --- a/test/ethStaking/operator-controller.test.ts +++ b/test/ethStaking/operator-controller.test.ts @@ -37,7 +37,11 @@ describe('OperatorController', () => { }) beforeEach(async () => { - sdToken = (await deploy('ERC677', ['test', 'test', 50])) as ERC677 + sdToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'test', + 'test', + 50, + ])) as ERC677 controller = (await deployUpgradeable('OperatorControllerMock', [ accounts[0], sdToken.address, diff --git a/test/ethStaking/wl-operator-controller.test.ts b/test/ethStaking/wl-operator-controller.test.ts index b7d89085..1b168176 100644 --- a/test/ethStaking/wl-operator-controller.test.ts +++ b/test/ethStaking/wl-operator-controller.test.ts @@ -40,7 +40,11 @@ describe('WLOperatorController', () => { let operatorWhitelist = (await deploy('OperatorWhitelistMock', [ [accounts[0]], ])) as OperatorWhitelistMock - wsdToken = (await deploy('ERC677', ['test', 'test', 100000])) as ERC677 + wsdToken = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'test', + 'test', + 100000, + ])) as ERC677 controller = (await deployUpgradeable('WLOperatorController', [ accounts[0], wsdToken.address, diff --git a/test/linkStaking/community-vault.test.ts b/test/linkStaking/community-vault.test.ts index d8547993..158f195b 100644 --- a/test/linkStaking/community-vault.test.ts +++ b/test/linkStaking/community-vault.test.ts @@ -17,7 +17,11 @@ describe('CommunityVault', () => { }) beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 rewardsController = (await deploy('StakingRewardsMock', [token.address])) as StakingRewardsMock stakingController = (await deploy('StakingMock', [ diff --git a/test/linkStaking/community-vcs.test.ts b/test/linkStaking/community-vcs.test.ts index e82cc3b5..7f67cf4c 100644 --- a/test/linkStaking/community-vcs.test.ts +++ b/test/linkStaking/community-vcs.test.ts @@ -23,7 +23,11 @@ describe('CommunityVCS', () => { }) beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 await setupToken(token, accounts) rewardsController = (await deploy('StakingRewardsMock', [token.address])) as StakingRewardsMock diff --git a/test/linkStaking/operator-vault.test.ts b/test/linkStaking/operator-vault.test.ts index aafaaac0..adad573b 100644 --- a/test/linkStaking/operator-vault.test.ts +++ b/test/linkStaking/operator-vault.test.ts @@ -26,7 +26,11 @@ describe('OperatorVault', () => { }) beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 rewardsController = (await deploy('StakingRewardsMock', [token.address])) as StakingRewardsMock stakingController = (await deploy('StakingMock', [ diff --git a/test/linkStaking/operator-vcs.test.ts b/test/linkStaking/operator-vcs.test.ts index 6a8c317c..93f1ea46 100644 --- a/test/linkStaking/operator-vcs.test.ts +++ b/test/linkStaking/operator-vcs.test.ts @@ -36,7 +36,11 @@ describe('OperatorVCS', () => { }) beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 rewardsController = (await deploy('StakingRewardsMock', [token.address])) as StakingRewardsMock stakingController = (await deploy('StakingMock', [ diff --git a/test/linkStaking/vault-controller-strategy.test.ts b/test/linkStaking/vault-controller-strategy.test.ts index 50fdd5f1..7aac7d69 100644 --- a/test/linkStaking/vault-controller-strategy.test.ts +++ b/test/linkStaking/vault-controller-strategy.test.ts @@ -34,7 +34,11 @@ describe('VaultControllerStrategy', () => { }) beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 await setupToken(token, accounts) rewardsController = (await deploy('StakingRewardsMock', [token.address])) as StakingRewardsMock diff --git a/test/linkStaking/vault.test.ts b/test/linkStaking/vault.test.ts index 62351041..995873ad 100644 --- a/test/linkStaking/vault.test.ts +++ b/test/linkStaking/vault.test.ts @@ -22,7 +22,11 @@ describe('Vault', () => { }) beforeEach(async () => { - token = (await deploy('ERC677', ['Chainlink', 'LINK', 1000000000])) as ERC677 + token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 await setupToken(token, accounts) rewardsController = (await deploy('StakingRewardsMock', [token.address])) as StakingRewardsMock diff --git a/test/liquidSDIndex/liquid-sd-adapter.test.ts b/test/liquidSDIndex/liquid-sd-adapter.test.ts index 35b86305..2ca1e61d 100644 --- a/test/liquidSDIndex/liquid-sd-adapter.test.ts +++ b/test/liquidSDIndex/liquid-sd-adapter.test.ts @@ -14,7 +14,11 @@ describe('LSDIndexAdapter', () => { }) beforeEach(async () => { - lsd = (await deploy('ERC677', ['Liquid SD Token', 'LSD', 100000000])) as ERC677 + lsd = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Liquid SD Token', + 'LSD', + 100000000, + ])) as ERC677 adapter = (await deployUpgradeable('LSDIndexAdapterMock', [ lsd.address, accounts[0], diff --git a/test/liquidSDIndex/liquid-sd-index-pool.test.ts b/test/liquidSDIndex/liquid-sd-index-pool.test.ts index 9b1f1920..f9e233ea 100644 --- a/test/liquidSDIndex/liquid-sd-index-pool.test.ts +++ b/test/liquidSDIndex/liquid-sd-index-pool.test.ts @@ -25,8 +25,16 @@ describe('LiquidSDIndexPool', () => { }) beforeEach(async () => { - lsd1 = (await deploy('ERC677', ['Liquid SD Token 1', 'LSD1', 100000000])) as ERC677 - lsd2 = (await deploy('ERC677', ['Liquid SD Token 2', 'LSD2', 100000000])) as ERC677 + lsd1 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Liquid SD Token 1', + 'LSD1', + 100000000, + ])) as ERC677 + lsd2 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Liquid SD Token 2', + 'LSD2', + 100000000, + ])) as ERC677 await setupToken(lsd1, accounts) await setupToken(lsd2, accounts) @@ -64,7 +72,11 @@ describe('LiquidSDIndexPool', () => { }) it('addLSDToken should work correctly', async () => { - let lsd3 = (await deploy('ERC677', ['Liquid SD Token 2', 'LSD2', 100000000])) as ERC677 + let lsd3 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Liquid SD Token 2', + 'LSD2', + 100000000, + ])) as ERC677 let adapter3 = (await deployUpgradeable('LSDIndexAdapterMock', [ lsd3.address, pool.address, @@ -93,7 +105,11 @@ describe('LiquidSDIndexPool', () => { }) it('removeLSDToken should work correctly', async () => { - let lsd3 = (await deploy('ERC677', ['Liquid SD Token 2', 'LSD2', 100000000])) as ERC677 + let lsd3 = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Liquid SD Token 2', + 'LSD2', + 100000000, + ])) as ERC677 let adapter3 = (await deployUpgradeable('LSDIndexAdapterMock', [ lsd3.address, pool.address, From f4c5d4fef11455decabd54892cf08d8589664643 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Sun, 28 Jan 2024 20:53:49 +1300 Subject: [PATCH 30/42] updated @chainlink/contracts-ccip --- package.json | 2 +- yarn.lock | 529 +++++++++++---------------------------------------- 2 files changed, 113 insertions(+), 418 deletions(-) diff --git a/package.json b/package.json index a4f426c0..92a83a50 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "@chainlink/contracts": "0.8.0", - "@chainlink/contracts-ccip": "^0.7.6", + "@chainlink/contracts-ccip": "^1.2.1", "@openzeppelin/contracts": "^4.7.0", "@openzeppelin/contracts-upgradeable": "^4.9.2", "@prb/math": "^2.5.0", diff --git a/yarn.lock b/yarn.lock index 9038f44e..636cf8fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -201,10 +201,10 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" -"@chainlink/contracts-ccip@^0.7.6": - version "0.7.6" - resolved "https://registry.yarnpkg.com/@chainlink/contracts-ccip/-/contracts-ccip-0.7.6.tgz#5bf4568a0bbf4e29d2e8c32348e5ecc6ced006d2" - integrity sha512-yNbCBFpLs3R+ALymto9dQYKz3vatnjqYGu1pnMD0i2fHEMthiXe0+otaNCGNht6n8k7ruNaA0DNpz3F+2jHQXw== +"@chainlink/contracts-ccip@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@chainlink/contracts-ccip/-/contracts-ccip-1.2.1.tgz#534e7fb13d066cc4e1902e6d7bca189c24092b36" + integrity sha512-8lVod5Gclx25ZSLqX40zzhMwN7unnvj9AMKOE/LYIP5DjyiTDs/3BeXTw6GakeIkQF5v3FILnMIz8emF5FdSpQ== dependencies: "@eth-optimism/contracts" "^0.5.21" "@openzeppelin/contracts" "~4.3.3" @@ -250,27 +250,35 @@ integrity sha512-bvaTH34PMCbv6anRa9I/0zjLJgY4EuznbEMgbV77JBCQ9KNC46rzi0avuxpOfu+xDjPEtSFGqVEOr5GlUSGudA== "@eth-optimism/contracts@^0.5.21": - version "0.5.32" - resolved "https://registry.yarnpkg.com/@eth-optimism/contracts/-/contracts-0.5.32.tgz#3cc1ba823e9e71662954321025267522b0e928b8" - integrity sha512-wHcilwbQGxllfl43Has5l8PAOghtukRgwdhnQqrdR+XshHwlgnNH5lE5/48BjqSJDykxovmqP9rjQvHEWd5qSw== - dependencies: - "@eth-optimism/core-utils" "0.9.3" - "@ethersproject/abstract-provider" "^5.6.1" - "@ethersproject/abstract-signer" "^5.6.2" - -"@eth-optimism/core-utils@0.9.3": - version "0.9.3" - resolved "https://registry.yarnpkg.com/@eth-optimism/core-utils/-/core-utils-0.9.3.tgz#40c0271f815af68e0a4715e97a045a96462df7b6" - integrity sha512-b3V8qBgM0e85wdp3CNdJ6iSUvjT2k86F9oCAYeCIXQcQ6+EPaetjxP0T6ct6jLVepnJjoPRlW/lvWslKk1UBGg== - dependencies: - "@ethersproject/abstract-provider" "^5.6.1" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/providers" "^5.6.8" - "@ethersproject/transactions" "^5.6.2" - "@ethersproject/web" "^5.6.1" + version "0.5.40" + resolved "https://registry.yarnpkg.com/@eth-optimism/contracts/-/contracts-0.5.40.tgz#d13a04a15ea947a69055e6fc74d87e215d4c936a" + integrity sha512-MrzV0nvsymfO/fursTB7m/KunkPsCndltVgfdHaT1Aj5Vi6R/doKIGGkOofHX+8B6VMZpuZosKCMQ5lQuqjt8w== + dependencies: + "@eth-optimism/core-utils" "0.12.0" + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + +"@eth-optimism/core-utils@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@eth-optimism/core-utils/-/core-utils-0.12.0.tgz#6337e4599a34de23f8eceb20378de2a2de82b0ea" + integrity sha512-qW+7LZYCz7i8dRa7SRlUKIo1VBU8lvN0HeXCxJR+z+xtMzMQpPds20XJNCMclszxYQHkXY00fOT6GvFw9ZL6nw== + dependencies: + "@ethersproject/abi" "^5.7.0" + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/contracts" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/providers" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" bufio "^1.0.7" chai "^4.3.4" - ethers "^5.6.8" "@ethereum-waffle/chai@^3.4.0": version "3.4.1" @@ -409,22 +417,7 @@ "@ethersproject/properties" "^5.5.0" "@ethersproject/strings" "^5.5.0" -"@ethersproject/abi@5.6.4", "@ethersproject/abi@^5.6.3": - version "5.6.4" - resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.6.4.tgz#f6e01b6ed391a505932698ecc0d9e7a99ee60362" - integrity sha512-TTeZUlCeIHG6527/2goZA6gW5F8Emoc7MrZDC7hhP84aRGvW3TEdTnZR08Ls88YXM1m2SuK42Osw/jSi3uO8gg== - dependencies: - "@ethersproject/address" "^5.6.1" - "@ethersproject/bignumber" "^5.6.2" - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/constants" "^5.6.1" - "@ethersproject/hash" "^5.6.1" - "@ethersproject/keccak256" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/strings" "^5.6.1" - -"@ethersproject/abi@^5.7.0": +"@ethersproject/abi@^5.6.3", "@ethersproject/abi@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== @@ -452,19 +445,6 @@ "@ethersproject/transactions" "^5.5.0" "@ethersproject/web" "^5.5.0" -"@ethersproject/abstract-provider@5.6.1", "@ethersproject/abstract-provider@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.6.1.tgz#02ddce150785caf0c77fe036a0ebfcee61878c59" - integrity sha512-BxlIgogYJtp1FS8Muvj8YfdClk3unZH0vRMVX791Z9INBNT/kuACZ9GzaY1Y4yFq+YSy6/w4gzj3HCRKrK9hsQ== - dependencies: - "@ethersproject/bignumber" "^5.6.2" - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/networks" "^5.6.3" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/transactions" "^5.6.2" - "@ethersproject/web" "^5.6.1" - "@ethersproject/abstract-provider@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef" @@ -489,17 +469,6 @@ "@ethersproject/logger" "^5.5.0" "@ethersproject/properties" "^5.5.0" -"@ethersproject/abstract-signer@5.6.2", "@ethersproject/abstract-signer@^5.6.2": - version "5.6.2" - resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.6.2.tgz#491f07fc2cbd5da258f46ec539664713950b0b33" - integrity sha512-n1r6lttFBG0t2vNiI3HoWaS/KdOt8xyDjzlP2cuevlWLG6EX0OwcKLyG/Kp/cuwNxdy/ous+R/DEMdTUwWQIjQ== - dependencies: - "@ethersproject/abstract-provider" "^5.6.1" - "@ethersproject/bignumber" "^5.6.2" - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/abstract-signer@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2" @@ -522,17 +491,6 @@ "@ethersproject/logger" "^5.5.0" "@ethersproject/rlp" "^5.5.0" -"@ethersproject/address@5.6.1", "@ethersproject/address@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.6.1.tgz#ab57818d9aefee919c5721d28cd31fd95eff413d" - integrity sha512-uOgF0kS5MJv9ZvCz7x6T2EXJSzotiybApn4XlOgoTX0xdtyVIJ7pF+6cGPxiEq/dpBiTfMiw7Yc81JcwhSYA0Q== - dependencies: - "@ethersproject/bignumber" "^5.6.2" - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/keccak256" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/rlp" "^5.6.1" - "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" @@ -551,13 +509,6 @@ dependencies: "@ethersproject/bytes" "^5.5.0" -"@ethersproject/base64@5.6.1", "@ethersproject/base64@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.6.1.tgz#2c40d8a0310c9d1606c2c37ae3092634b41d87cb" - integrity sha512-qB76rjop6a0RIYYMiB4Eh/8n+Hxu2NIZm8S/Q7kNo5pmZfXhHGHmS4MinUainiBC54SCyRnwzL+KZjj8zbsSsw== - dependencies: - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/base64@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" @@ -573,13 +524,13 @@ "@ethersproject/bytes" "^5.5.0" "@ethersproject/properties" "^5.5.0" -"@ethersproject/basex@5.6.1", "@ethersproject/basex@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.6.1.tgz#badbb2f1d4a6f52ce41c9064f01eab19cc4c5305" - integrity sha512-a52MkVz4vuBXR06nvflPMotld1FJWSj2QT0985v7P/emPZO00PucFAkbcmq2vpVU7Ts7umKiSI6SppiLykVWsA== +"@ethersproject/basex@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.7.0.tgz#97034dc7e8938a8ca943ab20f8a5e492ece4020b" + integrity sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw== dependencies: - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/properties" "^5.6.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/properties" "^5.7.0" "@ethersproject/bignumber@5.5.0", "@ethersproject/bignumber@>=5.0.0-beta.130", "@ethersproject/bignumber@^5.5.0": version "5.5.0" @@ -590,15 +541,6 @@ "@ethersproject/logger" "^5.5.0" bn.js "^4.11.9" -"@ethersproject/bignumber@5.6.2", "@ethersproject/bignumber@^5.6.2": - version "5.6.2" - resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.6.2.tgz#72a0717d6163fab44c47bcc82e0c550ac0315d66" - integrity sha512-v7+EEUbhGqT3XJ9LMPsKvXYHFc8eHxTowFCG/HgJErmq4XHJ2WR7aeyICg3uTOAQ7Icn0GFHAohXEhxQHq4Ubw== - dependencies: - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - bn.js "^5.2.1" - "@ethersproject/bignumber@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" @@ -615,13 +557,6 @@ dependencies: "@ethersproject/logger" "^5.5.0" -"@ethersproject/bytes@5.6.1", "@ethersproject/bytes@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.6.1.tgz#24f916e411f82a8a60412344bf4a813b917eefe7" - integrity sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g== - dependencies: - "@ethersproject/logger" "^5.6.0" - "@ethersproject/bytes@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" @@ -636,13 +571,6 @@ dependencies: "@ethersproject/bignumber" "^5.5.0" -"@ethersproject/constants@5.6.1", "@ethersproject/constants@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.6.1.tgz#e2e974cac160dd101cf79fdf879d7d18e8cb1370" - integrity sha512-QSq9WVnZbxXYFftrjSjZDUshp6/eKp6qrtdBtUCm0QxCV5z1fG/w3kdlcsjMCQuQHUnAclKoK7XpXMezhRDOLg== - dependencies: - "@ethersproject/bignumber" "^5.6.2" - "@ethersproject/constants@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" @@ -666,22 +594,6 @@ "@ethersproject/properties" "^5.5.0" "@ethersproject/transactions" "^5.5.0" -"@ethersproject/contracts@5.6.2": - version "5.6.2" - resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.6.2.tgz#20b52e69ebc1b74274ff8e3d4e508de971c287bc" - integrity sha512-hguUA57BIKi6WY0kHvZp6PwPlWF87MCeB4B7Z7AbUpTxfFXFdn/3b0GmjZPagIHS+3yhcBJDnuEfU4Xz+Ks/8g== - dependencies: - "@ethersproject/abi" "^5.6.3" - "@ethersproject/abstract-provider" "^5.6.1" - "@ethersproject/abstract-signer" "^5.6.2" - "@ethersproject/address" "^5.6.1" - "@ethersproject/bignumber" "^5.6.2" - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/constants" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/transactions" "^5.6.2" - "@ethersproject/contracts@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e" @@ -712,20 +624,6 @@ "@ethersproject/properties" "^5.5.0" "@ethersproject/strings" "^5.5.0" -"@ethersproject/hash@5.6.1", "@ethersproject/hash@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.6.1.tgz#224572ea4de257f05b4abf8ae58b03a67e99b0f4" - integrity sha512-L1xAHurbaxG8VVul4ankNX5HgQ8PNCTrnVXEiFnE9xoRnaUcgfD12tZINtDinSllxPLCtGwguQxJ5E6keE84pA== - dependencies: - "@ethersproject/abstract-signer" "^5.6.2" - "@ethersproject/address" "^5.6.1" - "@ethersproject/bignumber" "^5.6.2" - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/keccak256" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/strings" "^5.6.1" - "@ethersproject/hash@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" @@ -759,24 +657,6 @@ "@ethersproject/transactions" "^5.5.0" "@ethersproject/wordlists" "^5.5.0" -"@ethersproject/hdnode@5.6.2", "@ethersproject/hdnode@^5.6.2": - version "5.6.2" - resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.6.2.tgz#26f3c83a3e8f1b7985c15d1db50dc2903418b2d2" - integrity sha512-tERxW8Ccf9CxW2db3WsN01Qao3wFeRsfYY9TCuhmG0xNpl2IO8wgXU3HtWIZ49gUWPggRy4Yg5axU0ACaEKf1Q== - dependencies: - "@ethersproject/abstract-signer" "^5.6.2" - "@ethersproject/basex" "^5.6.1" - "@ethersproject/bignumber" "^5.6.2" - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/pbkdf2" "^5.6.1" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/sha2" "^5.6.1" - "@ethersproject/signing-key" "^5.6.2" - "@ethersproject/strings" "^5.6.1" - "@ethersproject/transactions" "^5.6.2" - "@ethersproject/wordlists" "^5.6.1" - "@ethersproject/json-wallets@5.5.0", "@ethersproject/json-wallets@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.5.0.tgz#dd522d4297e15bccc8e1427d247ec8376b60e325" @@ -796,25 +676,6 @@ aes-js "3.0.0" scrypt-js "3.0.1" -"@ethersproject/json-wallets@5.6.1", "@ethersproject/json-wallets@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.6.1.tgz#3f06ba555c9c0d7da46756a12ac53483fe18dd91" - integrity sha512-KfyJ6Zwz3kGeX25nLihPwZYlDqamO6pfGKNnVMWWfEVVp42lTfCZVXXy5Ie8IZTN0HKwAngpIPi7gk4IJzgmqQ== - dependencies: - "@ethersproject/abstract-signer" "^5.6.2" - "@ethersproject/address" "^5.6.1" - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/hdnode" "^5.6.2" - "@ethersproject/keccak256" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/pbkdf2" "^5.6.1" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/random" "^5.6.1" - "@ethersproject/strings" "^5.6.1" - "@ethersproject/transactions" "^5.6.2" - aes-js "3.0.0" - scrypt-js "3.0.1" - "@ethersproject/keccak256@5.5.0", "@ethersproject/keccak256@>=5.0.0-beta.127", "@ethersproject/keccak256@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.5.0.tgz#e4b1f9d7701da87c564ffe336f86dcee82983492" @@ -823,14 +684,6 @@ "@ethersproject/bytes" "^5.5.0" js-sha3 "0.8.0" -"@ethersproject/keccak256@5.6.1", "@ethersproject/keccak256@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.6.1.tgz#b867167c9b50ba1b1a92bccdd4f2d6bd168a91cc" - integrity sha512-bB7DQHCTRDooZZdL3lk9wpL0+XuG3XLGHLh3cePnybsO3V0rdCAOQGpn/0R3aODmnTOOkCATJiD2hnL+5bwthA== - dependencies: - "@ethersproject/bytes" "^5.6.1" - js-sha3 "0.8.0" - "@ethersproject/keccak256@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" @@ -844,11 +697,6 @@ resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.5.0.tgz#0c2caebeff98e10aefa5aef27d7441c7fd18cf5d" integrity sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg== -"@ethersproject/logger@5.6.0", "@ethersproject/logger@^5.6.0": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.6.0.tgz#d7db1bfcc22fd2e4ab574cba0bb6ad779a9a3e7a" - integrity sha512-BiBWllUROH9w+P21RzoxJKzqoqpkyM1pRnEKG69bulE9TSQD8SAIvTQqIMZmmCO8pUNkgLP1wndX1gKghSpBmg== - "@ethersproject/logger@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" @@ -861,13 +709,6 @@ dependencies: "@ethersproject/logger" "^5.5.0" -"@ethersproject/networks@5.6.4", "@ethersproject/networks@^5.6.3": - version "5.6.4" - resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.6.4.tgz#51296d8fec59e9627554f5a8a9c7791248c8dc07" - integrity sha512-KShHeHPahHI2UlWdtDMn2lJETcbtaJge4k7XSjDR9h79QTd6yQJmv6Cp2ZA4JdqWnhszAOLSuJEd9C0PRw7hSQ== - dependencies: - "@ethersproject/logger" "^5.6.0" - "@ethersproject/networks@^5.7.0": version "5.7.1" resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" @@ -883,14 +724,6 @@ "@ethersproject/bytes" "^5.5.0" "@ethersproject/sha2" "^5.5.0" -"@ethersproject/pbkdf2@5.6.1", "@ethersproject/pbkdf2@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.6.1.tgz#f462fe320b22c0d6b1d72a9920a3963b09eb82d1" - integrity sha512-k4gRQ+D93zDRPNUfmduNKq065uadC2YjMP/CqwwX5qG6R05f47boq6pLZtV/RnC4NZAYOPH1Cyo54q0c9sshRQ== - dependencies: - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/sha2" "^5.6.1" - "@ethersproject/properties@5.5.0", "@ethersproject/properties@>=5.0.0-beta.131", "@ethersproject/properties@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.5.0.tgz#61f00f2bb83376d2071baab02245f92070c59995" @@ -898,13 +731,6 @@ dependencies: "@ethersproject/logger" "^5.5.0" -"@ethersproject/properties@5.6.0", "@ethersproject/properties@^5.6.0": - version "5.6.0" - resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.6.0.tgz#38904651713bc6bdd5bdd1b0a4287ecda920fa04" - integrity sha512-szoOkHskajKePTJSZ46uHUWWkbv7TzP2ypdEK6jGMqJaEt2sb0jCgfBo0gH0m2HBpRixMuJ6TBRaQCF7a9DoCg== - dependencies: - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30" @@ -937,29 +763,29 @@ bech32 "1.1.4" ws "7.4.6" -"@ethersproject/providers@5.6.8", "@ethersproject/providers@^5.6.8": - version "5.6.8" - resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.6.8.tgz#22e6c57be215ba5545d3a46cf759d265bb4e879d" - integrity sha512-Wf+CseT/iOJjrGtAOf3ck9zS7AgPmr2fZ3N97r4+YXN3mBePTG2/bJ8DApl9mVwYL+RpYbNxMEkEp4mPGdwG/w== - dependencies: - "@ethersproject/abstract-provider" "^5.6.1" - "@ethersproject/abstract-signer" "^5.6.2" - "@ethersproject/address" "^5.6.1" - "@ethersproject/base64" "^5.6.1" - "@ethersproject/basex" "^5.6.1" - "@ethersproject/bignumber" "^5.6.2" - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/constants" "^5.6.1" - "@ethersproject/hash" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/networks" "^5.6.3" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/random" "^5.6.1" - "@ethersproject/rlp" "^5.6.1" - "@ethersproject/sha2" "^5.6.1" - "@ethersproject/strings" "^5.6.1" - "@ethersproject/transactions" "^5.6.2" - "@ethersproject/web" "^5.6.1" +"@ethersproject/providers@^5.7.0": + version "5.7.2" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" + integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/basex" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" bech32 "1.1.4" ws "7.4.6" @@ -971,13 +797,13 @@ "@ethersproject/bytes" "^5.5.0" "@ethersproject/logger" "^5.5.0" -"@ethersproject/random@5.6.1", "@ethersproject/random@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.6.1.tgz#66915943981bcd3e11bbd43733f5c3ba5a790255" - integrity sha512-/wtPNHwbmng+5yi3fkipA8YBT59DdkGRoC2vWk09Dci/q5DlgnMkhIycjHlavrvrjJBkFjO/ueLyT+aUDfc4lA== +"@ethersproject/random@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.7.0.tgz#af19dcbc2484aae078bb03656ec05df66253280c" + integrity sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ== dependencies: - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/logger" "^5.6.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" "@ethersproject/rlp@5.5.0", "@ethersproject/rlp@^5.5.0": version "5.5.0" @@ -987,14 +813,6 @@ "@ethersproject/bytes" "^5.5.0" "@ethersproject/logger" "^5.5.0" -"@ethersproject/rlp@5.6.1", "@ethersproject/rlp@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.6.1.tgz#df8311e6f9f24dcb03d59a2bac457a28a4fe2bd8" - integrity sha512-uYjmcZx+DKlFUk7a5/W9aQVaoEC7+1MOBgNtvNg13+RnuUwT4F0zTovC0tmay5SmRslb29V1B7Y5KCri46WhuQ== - dependencies: - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/rlp@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" @@ -1012,15 +830,6 @@ "@ethersproject/logger" "^5.5.0" hash.js "1.1.7" -"@ethersproject/sha2@5.6.1", "@ethersproject/sha2@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.6.1.tgz#211f14d3f5da5301c8972a8827770b6fd3e51656" - integrity sha512-5K2GyqcW7G4Yo3uenHegbXRPDgARpWUiXc6RiF7b6i/HXUoWlb7uCARh7BAHg7/qT/Q5ydofNwiZcim9qpjB6g== - dependencies: - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - hash.js "1.1.7" - "@ethersproject/sha2@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.7.0.tgz#9a5f7a7824ef784f7f7680984e593a800480c9fb" @@ -1042,18 +851,6 @@ elliptic "6.5.4" hash.js "1.1.7" -"@ethersproject/signing-key@5.6.2", "@ethersproject/signing-key@^5.6.2": - version "5.6.2" - resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.6.2.tgz#8a51b111e4d62e5a62aee1da1e088d12de0614a3" - integrity sha512-jVbu0RuP7EFpw82vHcL+GP35+KaNruVAZM90GxgQnGqB6crhBqW/ozBfFvdeImtmb4qPko0uxXjn8l9jpn0cwQ== - dependencies: - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - bn.js "^5.2.1" - elliptic "6.5.4" - hash.js "1.1.7" - "@ethersproject/signing-key@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3" @@ -1078,18 +875,6 @@ "@ethersproject/sha2" "^5.5.0" "@ethersproject/strings" "^5.5.0" -"@ethersproject/solidity@5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.6.1.tgz#5845e71182c66d32e6ec5eefd041fca091a473e2" - integrity sha512-KWqVLkUUoLBfL1iwdzUVlkNqAUIFMpbbeH0rgCfKmJp0vFtY4AsaN91gHKo9ZZLkC4UOm3cI3BmMV4N53BOq4g== - dependencies: - "@ethersproject/bignumber" "^5.6.2" - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/keccak256" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/sha2" "^5.6.1" - "@ethersproject/strings" "^5.6.1" - "@ethersproject/solidity@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.7.0.tgz#5e9c911d8a2acce2a5ebb48a5e2e0af20b631cb8" @@ -1111,15 +896,6 @@ "@ethersproject/constants" "^5.5.0" "@ethersproject/logger" "^5.5.0" -"@ethersproject/strings@5.6.1", "@ethersproject/strings@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.6.1.tgz#dbc1b7f901db822b5cafd4ebf01ca93c373f8952" - integrity sha512-2X1Lgk6Jyfg26MUnsHiT456U9ijxKUybz8IM1Vih+NJxYtXhmvKBcHOmvGqpFSVJ0nQ4ZCoIViR8XlRw1v/+Cw== - dependencies: - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/constants" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/strings@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2" @@ -1144,22 +920,7 @@ "@ethersproject/rlp" "^5.5.0" "@ethersproject/signing-key" "^5.5.0" -"@ethersproject/transactions@5.6.2", "@ethersproject/transactions@^5.6.2": - version "5.6.2" - resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.6.2.tgz#793a774c01ced9fe7073985bb95a4b4e57a6370b" - integrity sha512-BuV63IRPHmJvthNkkt9G70Ullx6AcM+SDc+a8Aw/8Yew6YwT51TcBKEp1P4oOQ/bP25I18JJr7rcFRgFtU9B2Q== - dependencies: - "@ethersproject/address" "^5.6.1" - "@ethersproject/bignumber" "^5.6.2" - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/constants" "^5.6.1" - "@ethersproject/keccak256" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/rlp" "^5.6.1" - "@ethersproject/signing-key" "^5.6.2" - -"@ethersproject/transactions@^5.7.0": +"@ethersproject/transactions@^5.6.2", "@ethersproject/transactions@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b" integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ== @@ -1183,15 +944,6 @@ "@ethersproject/constants" "^5.5.0" "@ethersproject/logger" "^5.5.0" -"@ethersproject/units@5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.6.1.tgz#ecc590d16d37c8f9ef4e89e2005bda7ddc6a4e6f" - integrity sha512-rEfSEvMQ7obcx3KWD5EWWx77gqv54K6BKiZzKxkQJqtpriVsICrktIQmKl8ReNToPeIYPnFHpXvKpi068YFZXw== - dependencies: - "@ethersproject/bignumber" "^5.6.2" - "@ethersproject/constants" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/wallet@5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.5.0.tgz#322a10527a440ece593980dca6182f17d54eae75" @@ -1213,27 +965,6 @@ "@ethersproject/transactions" "^5.5.0" "@ethersproject/wordlists" "^5.5.0" -"@ethersproject/wallet@5.6.2": - version "5.6.2" - resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.6.2.tgz#cd61429d1e934681e413f4bc847a5f2f87e3a03c" - integrity sha512-lrgh0FDQPuOnHcF80Q3gHYsSUODp6aJLAdDmDV0xKCN/T7D99ta1jGVhulg3PY8wiXEngD0DfM0I2XKXlrqJfg== - dependencies: - "@ethersproject/abstract-provider" "^5.6.1" - "@ethersproject/abstract-signer" "^5.6.2" - "@ethersproject/address" "^5.6.1" - "@ethersproject/bignumber" "^5.6.2" - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/hash" "^5.6.1" - "@ethersproject/hdnode" "^5.6.2" - "@ethersproject/json-wallets" "^5.6.1" - "@ethersproject/keccak256" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/random" "^5.6.1" - "@ethersproject/signing-key" "^5.6.2" - "@ethersproject/transactions" "^5.6.2" - "@ethersproject/wordlists" "^5.6.1" - "@ethersproject/web@5.5.1", "@ethersproject/web@^5.5.0": version "5.5.1" resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.5.1.tgz#cfcc4a074a6936c657878ac58917a61341681316" @@ -1245,17 +976,6 @@ "@ethersproject/properties" "^5.5.0" "@ethersproject/strings" "^5.5.0" -"@ethersproject/web@5.6.1", "@ethersproject/web@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.6.1.tgz#6e2bd3ebadd033e6fe57d072db2b69ad2c9bdf5d" - integrity sha512-/vSyzaQlNXkO1WV+RneYKqCJwualcUdx/Z3gseVovZP0wIlOFcCE1hkRhKBH8ImKbGQbMl9EAAyJFrJu7V0aqA== - dependencies: - "@ethersproject/base64" "^5.6.1" - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/strings" "^5.6.1" - "@ethersproject/web@^5.7.0": version "5.7.1" resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" @@ -1278,17 +998,6 @@ "@ethersproject/properties" "^5.5.0" "@ethersproject/strings" "^5.5.0" -"@ethersproject/wordlists@5.6.1", "@ethersproject/wordlists@^5.6.1": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.6.1.tgz#1e78e2740a8a21e9e99947e47979d72e130aeda1" - integrity sha512-wiPRgBpNbNwCQFoCr8bcWO8o5I810cqO6mkdtKfLKFlLxeCWcnzDi4Alu8iyNzlhYuS9npCwivMbRWF19dyblw== - dependencies: - "@ethersproject/bytes" "^5.6.1" - "@ethersproject/hash" "^5.6.1" - "@ethersproject/logger" "^5.6.0" - "@ethersproject/properties" "^5.6.0" - "@ethersproject/strings" "^5.6.1" - "@jridgewell/gen-mapping@^0.3.0": version "0.3.1" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz#cf92a983c83466b8c0ce9124fadeaf09f7c66ea9" @@ -2305,7 +2014,7 @@ adm-zip@^0.4.16: aes-js@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" - integrity sha1-4h3xCtbCBTKVvLuNq0Cwnb6ofk0= + integrity sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw== aes-js@^3.1.1, aes-js@^3.1.2: version "3.1.2" @@ -3339,7 +3048,7 @@ braces@^3.0.1, braces@~3.0.2: brorand@^1.0.1, brorand@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== browser-level@^1.0.1: version "1.0.1" @@ -3505,9 +3214,9 @@ bufferutil@^4.0.1: node-gyp-build "^4.3.0" bufio@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/bufio/-/bufio-1.0.7.tgz#b7f63a1369a0829ed64cc14edf0573b3e382a33e" - integrity sha512-bd1dDQhiC+bEbEfg56IdBv7faWa6OipMs/AFFFvtFnB3wAYjlwQpQRZ0pm6ZkgtfL0pILRXhKxOiQj6UzoMR7A== + version "1.2.1" + resolved "https://registry.yarnpkg.com/bufio/-/bufio-1.2.1.tgz#8d4ab3ddfcd5faa90f996f922f9397d41cbaf2de" + integrity sha512-9oR3zNdupcg/Ge2sSHQF3GX+kmvL/fTPvD0nd5AGLq8SjUYnTz+SlFjK/GXidndbZtIj+pVKXiWeR9w6e9wKCA== busboy@^1.6.0: version "1.6.0" @@ -3666,7 +3375,20 @@ cbor@^9.0.1: dependencies: nofilter "^3.1.0" -chai@^4.3.4, chai@^4.3.6: +chai@^4.3.4: + version "4.4.1" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" + integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.0.8" + +chai@^4.3.6: version "4.3.6" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c" integrity sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q== @@ -3712,10 +3434,12 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -check-error@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" - integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= +check-error@^1.0.2, check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" checkpoint-store@^1.1.0: version "1.1.0" @@ -4236,6 +3960,13 @@ deep-eql@^3.0.1: dependencies: type-detect "^4.0.0" +deep-eql@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" + integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== + dependencies: + type-detect "^4.0.0" + deep-equal@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" @@ -5259,42 +4990,6 @@ ethers@^5.0.1, ethers@^5.0.2, ethers@^5.4.7, ethers@^5.5.2, ethers@^5.5.4: "@ethersproject/web" "5.5.1" "@ethersproject/wordlists" "5.5.0" -ethers@^5.6.8: - version "5.6.9" - resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.6.9.tgz#4e12f8dfcb67b88ae7a78a9519b384c23c576a4d" - integrity sha512-lMGC2zv9HC5EC+8r429WaWu3uWJUCgUCt8xxKCFqkrFuBDZXDYIdzDUECxzjf2BMF8IVBByY1EBoGSL3RTm8RA== - dependencies: - "@ethersproject/abi" "5.6.4" - "@ethersproject/abstract-provider" "5.6.1" - "@ethersproject/abstract-signer" "5.6.2" - "@ethersproject/address" "5.6.1" - "@ethersproject/base64" "5.6.1" - "@ethersproject/basex" "5.6.1" - "@ethersproject/bignumber" "5.6.2" - "@ethersproject/bytes" "5.6.1" - "@ethersproject/constants" "5.6.1" - "@ethersproject/contracts" "5.6.2" - "@ethersproject/hash" "5.6.1" - "@ethersproject/hdnode" "5.6.2" - "@ethersproject/json-wallets" "5.6.1" - "@ethersproject/keccak256" "5.6.1" - "@ethersproject/logger" "5.6.0" - "@ethersproject/networks" "5.6.4" - "@ethersproject/pbkdf2" "5.6.1" - "@ethersproject/properties" "5.6.0" - "@ethersproject/providers" "5.6.8" - "@ethersproject/random" "5.6.1" - "@ethersproject/rlp" "5.6.1" - "@ethersproject/sha2" "5.6.1" - "@ethersproject/signing-key" "5.6.2" - "@ethersproject/solidity" "5.6.1" - "@ethersproject/strings" "5.6.1" - "@ethersproject/transactions" "5.6.2" - "@ethersproject/units" "5.6.1" - "@ethersproject/wallet" "5.6.2" - "@ethersproject/web" "5.6.1" - "@ethersproject/wordlists" "5.6.1" - ethjs-unit@0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-unit/-/ethjs-unit-0.1.6.tgz#c665921e476e87bce2a9d588a6fe0405b2c41699" @@ -5830,10 +5525,10 @@ get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-func-name@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" - integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= +get-func-name@^2.0.0, get-func-name@^2.0.1, get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" @@ -6201,7 +5896,7 @@ heap@0.2.6: hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== dependencies: hash.js "^1.0.3" minimalistic-assert "^1.0.0" @@ -7303,12 +6998,12 @@ loose-envify@^1.0.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" -loupe@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.1.tgz#a2e1192c9f452e4e85089766da10ac8288383947" - integrity sha512-EN1D3jyVmaX4tnajVlfbREU4axL647hLec1h/PXAb8CPDMJiYitcWF2UeLVNttRqaIqQs4x+mRvXf+d+TlDrCA== +loupe@^2.3.1, loupe@^2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== dependencies: - get-func-name "^2.0.0" + get-func-name "^2.0.1" lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" @@ -7574,7 +7269,7 @@ minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== minimatch@5.0.1: version "5.0.1" @@ -9897,7 +9592,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5: +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== From dc86d4ae58024d844f875a23eba4a6d393c61b51 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Tue, 20 Feb 2024 17:29:00 +1300 Subject: [PATCH 31/42] bug fixes and improvements --- .../ccip/SDLPoolCCIPControllerPrimary.sol | 5 ++ .../ccip/SDLPoolCCIPControllerSecondary.sol | 10 ++- contracts/core/ccip/WrappedTokenBridge.sol | 32 +++++---- contracts/core/ccip/base/CCIPReceiver.sol | 66 +++++++++++++++++++ .../core/ccip/base/SDLPoolCCIPController.sol | 25 ++++--- .../core/sdlPool/LinearBoostController.sol | 28 ++++++-- contracts/core/sdlPool/SDLPoolPrimary.sol | 5 +- contracts/core/sdlPool/SDLPoolSecondary.sol | 14 ++-- contracts/core/sdlPool/base/SDLPool.sol | 8 +-- .../core/test/chainlink/CCIPOffRampMock.sol | 2 +- .../core/test/chainlink/CCIPOnRampMock.sol | 5 +- test/core/ccip/resdl-token-bridge.test.ts | 2 +- .../sdl-pool-ccip-controller-primary.test.ts | 10 ++- ...sdl-pool-ccip-controller-secondary.test.ts | 10 ++- test/core/ccip/wrapped-token-bridge.test.ts | 14 ++-- .../sdlPool/linear-boost-controller.test.ts | 6 +- test/core/sdlPool/sdl-pool-primary.test.ts | 3 +- test/core/sdlPool/sdl-pool-secondary.test.ts | 1 + 18 files changed, 192 insertions(+), 54 deletions(-) create mode 100644 contracts/core/ccip/base/CCIPReceiver.sol diff --git a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol index c7555ebe..e5dd5e66 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol @@ -8,6 +8,11 @@ interface ISDLPoolPrimary is ISDLPool { function handleIncomingUpdate(uint256 _numNewRESDLTokens, int256 _totalRESDLSupplyChange) external returns (uint256); } +/** + * @title SDL Pool CCIP Controller Secondary + * @notice Acts as interface between CCIP and primary SDL Pool + * @dev deployed only on primary chain + */ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { using SafeERC20 for IERC20; diff --git a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol index 591b752e..cdf32e89 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol @@ -11,6 +11,12 @@ interface ISDLPoolSecondary is ISDLPool { function shouldUpdate() external view returns (bool); } +/** + * @title SDL Pool CCIP Controller Secondary + * @notice Acts as interface between CCIP and secondary SDL Pools + * @dev deployed on secondary chains, should always hold a small protocol owned reSDL + * position to negate certain edge cases + */ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { using SafeERC20 for IERC20; @@ -72,16 +78,18 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { /** * @notice Handles the outgoing transfer of an reSDL token to the primary chain + * @param _destinationChainSelector id of the destination chain * @param _sender sender of the transfer * @param _tokenId id of token * @return the destination address * @return the token being transferred **/ function handleOutgoingRESDL( - uint64, + uint64 _destinationChainSelector, address _sender, uint256 _tokenId ) external override onlyBridge returns (address, ISDLPool.RESDLToken memory) { + if (_destinationChainSelector != primaryChainSelector) revert InvalidDestination(); return (primaryChainDestination, ISDLPoolSecondary(sdlPool).handleOutgoingRESDL(_sender, _tokenId, address(this))); } diff --git a/contracts/core/ccip/WrappedTokenBridge.sol b/contracts/core/ccip/WrappedTokenBridge.sol index 1f5eeaf7..4169e85e 100644 --- a/contracts/core/ccip/WrappedTokenBridge.sol +++ b/contracts/core/ccip/WrappedTokenBridge.sol @@ -3,11 +3,10 @@ pragma solidity 0.8.15; import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol"; import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; -import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../interfaces/IWrappedLST.sol"; +import "./base/CCIPReceiver.sol"; /** * @title Wrapped token bridge @@ -16,7 +15,7 @@ import "../interfaces/IWrappedLST.sol"; * - can wrap tokens and initiate a CCIP transfer of the wrapped tokens to a destination chain * - can receive a CCIP transfer of wrapped tokens, unwrap them, and send them to the receiver */ -contract WrappedTokenBridge is Ownable, CCIPReceiver { +contract WrappedTokenBridge is CCIPReceiver { using SafeERC20 for IERC20; IERC20 linkToken; @@ -119,13 +118,18 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { /** * @notice Returns the current fee for a token transfer * @param _destinationChainSelector id of destination chain + * @param _amount amount of tokens to transfer * @param _payNative whether fee should be paid natively or with LINK * @return fee current fee **/ - function getFee(uint64 _destinationChainSelector, bool _payNative) external view returns (uint256) { + function getFee( + uint64 _destinationChainSelector, + uint256 _amount, + bool _payNative + ) external view returns (uint256) { Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( address(this), - 1000 ether, + _amount, _payNative ? address(0) : address(linkToken) ); @@ -133,16 +137,20 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { } /** - * @notice Recovers tokens that were accidentally sent to this contract - * @param _tokens list of tokens to recover - * @param _receiver address to receive recovered tokens + * @notice Withdraws tokens held by this contract + * @param _tokens list of tokens to withdraw + * @param _amounts list of corresponding amounts to withdraw + * @param _receiver address to receive tokens **/ - function recoverTokens(address[] calldata _tokens, address _receiver) external onlyOwner { + function recoverTokens( + address[] calldata _tokens, + uint256[] calldata _amounts, + address _receiver + ) external onlyOwner { if (_receiver == address(0)) revert InvalidReceiver(); for (uint256 i = 0; i < _tokens.length; ++i) { - IERC20 tokenToTransfer = IERC20(_tokens[i]); - tokenToTransfer.safeTransfer(_receiver, tokenToTransfer.balanceOf(address(this))); + IERC20(_tokens[i]).safeTransfer(_receiver, _amounts[i]); } } @@ -220,7 +228,7 @@ contract WrappedTokenBridge is Ownable, CCIPReceiver { receiver: abi.encode(_receiver), data: "", tokenAmounts: tokenAmounts, - extraArgs: "0x", + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0})), feeToken: _feeTokenAddress }); diff --git a/contracts/core/ccip/base/CCIPReceiver.sol b/contracts/core/ccip/base/CCIPReceiver.sol new file mode 100644 index 00000000..f84e3482 --- /dev/null +++ b/contracts/core/ccip/base/CCIPReceiver.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import {IAny2EVMMessageReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IAny2EVMMessageReceiver.sol"; +import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +/// @title CCIPReceiver - Base contract for CCIP applications that can receive messages. +/// @dev copied from https://github.com/smartcontractkit and modified to make i_router settable +abstract contract CCIPReceiver is IAny2EVMMessageReceiver, IERC165, Ownable { + address internal i_router; + + constructor(address _router) { + if (_router == address(0)) revert InvalidRouter(address(0)); + i_router = _router; + } + + /// @notice IERC165 supports an interfaceId + /// @param _interfaceId The interfaceId to check + /// @return true if the interfaceId is supported + /// @dev Should indicate whether the contract implements IAny2EVMMessageReceiver + /// e.g. return interfaceId == type(IAny2EVMMessageReceiver).interfaceId || interfaceId == type(IERC165).interfaceId + /// This allows CCIP to check if ccipReceive is available before calling it. + /// If this returns false or reverts, only tokens are transferred to the receiver. + /// If this returns true, tokens are transferred and ccipReceive is called atomically. + /// Additionally, if the receiver address does not have code associated with + /// it at the time of execution (EXTCODESIZE returns 0), only tokens will be transferred. + function supportsInterface(bytes4 _interfaceId) public pure virtual override returns (bool) { + return _interfaceId == type(IAny2EVMMessageReceiver).interfaceId || _interfaceId == type(IERC165).interfaceId; + } + + /// @inheritdoc IAny2EVMMessageReceiver + function ccipReceive(Client.Any2EVMMessage calldata _message) external virtual override onlyRouter { + _ccipReceive(_message); + } + + /// @notice Override this function in your implementation. + /// @param _message Any2EVMMessage + function _ccipReceive(Client.Any2EVMMessage memory _message) internal virtual; + + ///////////////////////////////////////////////////////////////////// + // Plumbing + ///////////////////////////////////////////////////////////////////// + + /// @notice Return the current router + /// @return i_router address + function getRouter() public view returns (address) { + return address(i_router); + } + + /// @notice Sets the router + /// @param _router router address + function setRouter(address _router) external onlyOwner { + if (_router == address(0)) revert InvalidRouter(address(0)); + i_router = _router; + } + + error InvalidRouter(address router); + + /// @dev only calls from the set router are accepted. + modifier onlyRouter() { + if (msg.sender != address(i_router)) revert InvalidRouter(msg.sender); + _; + } +} diff --git a/contracts/core/ccip/base/SDLPoolCCIPController.sol b/contracts/core/ccip/base/SDLPoolCCIPController.sol index 7fe4d529..668f9c12 100644 --- a/contracts/core/ccip/base/SDLPoolCCIPController.sol +++ b/contracts/core/ccip/base/SDLPoolCCIPController.sol @@ -3,14 +3,17 @@ pragma solidity 0.8.15; import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol"; import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; -import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../../interfaces/IRESDLTokenBridge.sol"; import "../../interfaces/ISDLPool.sol"; +import "./CCIPReceiver.sol"; -abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { +/** + * @title SDL Pool CCIP Controller + * @notice Base contract for SDL Pool CCIP controllers + */ +abstract contract SDLPoolCCIPController is CCIPReceiver { using SafeERC20 for IERC20; IERC20 public immutable linkToken; @@ -110,16 +113,20 @@ abstract contract SDLPoolCCIPController is Ownable, CCIPReceiver { } /** - * @notice Recovers tokens that were accidentally sent to this contract - * @param _tokens list of tokens to recover - * @param _receiver address to receive recovered tokens + * @notice Withdraws tokens held by this contract + * @param _tokens list of tokens to withdraw + * @param _amounts list of corresponding amounts to withdraw + * @param _receiver address to receive tokens **/ - function recoverTokens(address[] calldata _tokens, address _receiver) external onlyOwner { + function recoverTokens( + address[] calldata _tokens, + uint256[] calldata _amounts, + address _receiver + ) external onlyOwner { if (_receiver == address(0)) revert InvalidReceiver(); for (uint256 i = 0; i < _tokens.length; ++i) { - IERC20 tokenToTransfer = IERC20(_tokens[i]); - tokenToTransfer.safeTransfer(_receiver, tokenToTransfer.balanceOf(address(this))); + IERC20(_tokens[i]).safeTransfer(_receiver, _amounts[i]); } } diff --git a/contracts/core/sdlPool/LinearBoostController.sol b/contracts/core/sdlPool/LinearBoostController.sol index a5b798a2..00ce500b 100644 --- a/contracts/core/sdlPool/LinearBoostController.sol +++ b/contracts/core/sdlPool/LinearBoostController.sol @@ -8,20 +8,28 @@ import "@openzeppelin/contracts/access/Ownable.sol"; * @notice Handles boost calculations */ contract LinearBoostController is Ownable { + uint64 public minLockingDuration; uint64 public maxLockingDuration; uint64 public maxBoost; - event SetMaxLockingDuration(uint256 _maxLockingDuration); - event SetMaxBoost(uint256 _maxBoost); + event SetMinLockingDuration(uint64 _minLockingDuration); + event SetMaxLockingDuration(uint64 _maxLockingDuration); + event SetMaxBoost(uint64 _maxBoost); - error MaxLockingDurationExceeded(); + error InvalidLockingDuration(); /** * @notice initializes the contract state + * @param _minLockingDuration minimum non-zero locking duration in seconds * @param _maxLockingDuration maximum locking duration in seconds * @param _maxBoost maximum boost multiplier */ - constructor(uint64 _maxLockingDuration, uint64 _maxBoost) { + constructor( + uint64 _minLockingDuration, + uint64 _maxLockingDuration, + uint64 _maxBoost + ) { + minLockingDuration = _minLockingDuration; maxLockingDuration = _maxLockingDuration; maxBoost = _maxBoost; } @@ -34,10 +42,20 @@ contract LinearBoostController is Ownable { * @return amount of boost balance received in addition to the unboosted balance */ function getBoostAmount(uint256 _amount, uint64 _lockingDuration) external view returns (uint256) { - if (_lockingDuration > maxLockingDuration) revert MaxLockingDurationExceeded(); + if ((_lockingDuration != 0 && _lockingDuration < minLockingDuration) || _lockingDuration > maxLockingDuration) + revert InvalidLockingDuration(); return (_amount * uint256(maxBoost) * uint256(_lockingDuration)) / uint256(maxLockingDuration); } + /** + * @notice sets the minimum non-zero locking duration + * @param _minLockingDuration min non-zero locking duration in seconds + */ + function setMinLockingDuration(uint64 _minLockingDuration) external onlyOwner { + minLockingDuration = _minLockingDuration; + emit SetMinLockingDuration(_minLockingDuration); + } + /** * @notice sets the maximum locking duration * @param _maxLockingDuration max locking duration in seconds diff --git a/contracts/core/sdlPool/SDLPoolPrimary.sol b/contracts/core/sdlPool/SDLPoolPrimary.sol index 86d7095e..3f6caccd 100644 --- a/contracts/core/sdlPool/SDLPoolPrimary.sol +++ b/contracts/core/sdlPool/SDLPoolPrimary.sol @@ -33,10 +33,11 @@ contract SDLPoolPrimary is SDLPool { address _sdlToken, address _boostController ) public reinitializer(2) { - if (delegatorPool == address(0)) { + if (ccipController == address(0)) { __SDLPoolBase_init(_name, _symbol, _sdlToken, _boostController); } else { delegatorPool = ccipController; + delete ccipController; } } @@ -186,6 +187,7 @@ contract SDLPoolPrimary is SDLPool { delete locks[_lockId].amount; delete lockOwners[_lockId]; balances[_sender] -= 1; + delete tokenApprovals[_lockId]; uint256 totalAmount = lock.amount + lock.boostAmount; effectiveBalances[_sender] -= totalAmount; @@ -231,6 +233,7 @@ contract SDLPoolPrimary is SDLPool { function handleIncomingUpdate(uint256 _numNewRESDLTokens, int256 _totalRESDLSupplyChange) external onlyCCIPController + updateRewards(ccipController) returns (uint256) { uint256 mintStartIndex; diff --git a/contracts/core/sdlPool/SDLPoolSecondary.sol b/contracts/core/sdlPool/SDLPoolSecondary.sol index 27150425..179a102c 100644 --- a/contracts/core/sdlPool/SDLPoolSecondary.sol +++ b/contracts/core/sdlPool/SDLPoolSecondary.sol @@ -49,6 +49,7 @@ contract SDLPoolSecondary is SDLPool { error UpdateInProgress(); error NoUpdateInProgress(); error TooManyQueuedLocks(); + error LockWithdrawn(); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -268,6 +269,7 @@ contract SDLPoolSecondary is SDLPool { delete locks[_lockId].amount; delete lockOwners[_lockId]; balances[_sender] -= 1; + delete tokenApprovals[_lockId]; uint256 totalAmount = lock.amount + lock.boostAmount; effectiveBalances[_sender] -= totalAmount; @@ -432,6 +434,8 @@ contract SDLPoolSecondary is SDLPool { uint64 _lockingDuration ) internal onlyLockOwner(_lockId, _owner) { Lock memory lock = _getQueuedLockState(_lockId); + if (lock.amount == 0) revert LockWithdrawn(); + LockUpdate memory lockUpdate = LockUpdate(updateBatchIndex, _updateLock(lock, _amount, _lockingDuration)); queuedLockUpdates[_lockId].push(lockUpdate); queuedRESDLSupplyChange += @@ -471,21 +475,21 @@ contract SDLPoolSecondary is SDLPool { delete locks[lockId]; delete lockOwners[lockId]; balances[_owner] -= 1; - if (tokenApprovals[lockId] != address(0)) delete tokenApprovals[lockId]; + delete tokenApprovals[lockId]; emit Transfer(_owner, address(0), lockId); } else { locks[lockId].amount = updateLockState.amount; } sdlToken.safeTransfer(_owner, uint256(-1 * baseAmountDiff)); - } else if (boostAmountDiff < 0) { + } else if (boostAmountDiff < 0 && updateLockState.boostAmount == 0) { locks[lockId].expiry = updateLockState.expiry; locks[lockId].boostAmount = 0; emit InitiateUnlock(_owner, lockId, updateLockState.expiry); } else { locks[lockId] = updateLockState; - uint256 totalDiff = uint256(baseAmountDiff + boostAmountDiff); - effectiveBalances[_owner] += totalDiff; - totalEffectiveBalance += totalDiff; + int256 totalDiff = baseAmountDiff + boostAmountDiff; + effectiveBalances[_owner] = uint256(int256(effectiveBalances[_owner]) + totalDiff); + totalEffectiveBalance = uint256(int256(totalEffectiveBalance) + totalDiff); emit UpdateLock( _owner, lockId, diff --git a/contracts/core/sdlPool/base/SDLPool.sol b/contracts/core/sdlPool/base/SDLPool.sol index f036770a..eaff71af 100644 --- a/contracts/core/sdlPool/base/SDLPool.sol +++ b/contracts/core/sdlPool/base/SDLPool.sol @@ -44,6 +44,8 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp string public baseURI; + uint256[3] __gap; + event InitiateUnlock(address indexed owner, uint256 indexed lockId, uint64 expiry); event Withdraw(address indexed owner, uint256 indexed lockId, uint256 amount); event CreateLock( @@ -67,9 +69,8 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp error InvalidLockId(); error InvalidLockingDuration(); error TransferFromIncorrectOwner(); - error TransferToZeroAddress(); + error TransferToInvalidAddress(); error TransferToNonERC721Implementer(); - error TransferToCCIPController(); error ApprovalToCurrentOwner(); error ApprovalToCaller(); error InvalidValue(); @@ -458,8 +459,7 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp uint256 _lockId ) internal virtual { if (_from != ownerOf(_lockId)) revert TransferFromIncorrectOwner(); - if (_to == address(0)) revert TransferToZeroAddress(); - if (_to == ccipController) revert TransferToCCIPController(); + if (_to == address(0) || _to == ccipController || _to == _from) revert TransferToInvalidAddress(); delete tokenApprovals[_lockId]; diff --git a/contracts/core/test/chainlink/CCIPOffRampMock.sol b/contracts/core/test/chainlink/CCIPOffRampMock.sol index eed6b8d4..b79ed557 100644 --- a/contracts/core/test/chainlink/CCIPOffRampMock.sol +++ b/contracts/core/test/chainlink/CCIPOffRampMock.sol @@ -42,7 +42,7 @@ contract CCIPOffRampMock { tokenPools[_tokenAmounts[i].token].releaseOrMint(_receiver, _tokenAmounts[i].amount); } - (bool success, ) = router.routeMessage( + (bool success, , ) = router.routeMessage( Client.Any2EVMMessage(_messageId, _sourceChainSelector, abi.encode(msg.sender), _data, _tokenAmounts), GAS_FOR_CALL_EXACT_CHECK, 1000000, diff --git a/contracts/core/test/chainlink/CCIPOnRampMock.sol b/contracts/core/test/chainlink/CCIPOnRampMock.sol index 11be30ed..6f6ef1c7 100644 --- a/contracts/core/test/chainlink/CCIPOnRampMock.sol +++ b/contracts/core/test/chainlink/CCIPOnRampMock.sol @@ -38,15 +38,16 @@ contract CCIPOnRampMock { return requestData[requestData.length - 1]; } - function getFee(Client.EVM2AnyMessage calldata _message) external view returns (uint256) { + function getFee(uint64, Client.EVM2AnyMessage calldata _message) external view returns (uint256) { return _message.feeToken == linkToken ? 2 ether : 3 ether; } - function getPoolBySourceToken(address _token) public view returns (address) { + function getPoolBySourceToken(uint64, address _token) public view returns (address) { return tokenPools[_token]; } function forwardFromRouter( + uint64, Client.EVM2AnyMessage calldata _message, uint256 _feeTokenAmount, address _originalSender diff --git a/test/core/ccip/resdl-token-bridge.test.ts b/test/core/ccip/resdl-token-bridge.test.ts index 1abf4999..95a434f3 100644 --- a/test/core/ccip/resdl-token-bridge.test.ts +++ b/test/core/ccip/resdl-token-bridge.test.ts @@ -68,7 +68,7 @@ describe('RESDLTokenBridge', () => { await router.applyRampUpdates([[77, onRamp.address]], [], [[77, offRamp.address]]) - let boostController = await deploy('LinearBoostController', [4 * 365 * 86400, 4]) + let boostController = await deploy('LinearBoostController', [10, 4 * 365 * 86400, 4]) sdlPool = (await deployUpgradeable('SDLPoolPrimary', [ 'reSDL', 'reSDL', diff --git a/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts b/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts index b84bdd4c..f0bed948 100644 --- a/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts +++ b/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts @@ -78,7 +78,7 @@ describe('SDLPoolCCIPControllerPrimary', () => { await router.applyRampUpdates([[77, onRamp.address]], [], [[77, offRamp.address]]) - let boostController = await deploy('LinearBoostController', [4 * 365 * 86400, 4]) + let boostController = await deploy('LinearBoostController', [10, 4 * 365 * 86400, 4]) sdlPool = (await deployUpgradeable('SDLPoolPrimary', [ 'reSDL', 'reSDL', @@ -396,9 +396,13 @@ describe('SDLPoolCCIPControllerPrimary', () => { it('recoverTokens should work correctly', async () => { await linkToken.transfer(controller.address, toEther(1000)) await sdlToken.transfer(controller.address, toEther(2000)) - await controller.recoverTokens([linkToken.address, sdlToken.address], accounts[3]) + await controller.recoverTokens( + [linkToken.address, sdlToken.address], + [toEther(1000), toEther(2000)], + accounts[3] + ) - assert.equal(fromEther(await linkToken.balanceOf(accounts[3])), 1100) + assert.equal(fromEther(await linkToken.balanceOf(accounts[3])), 1000) assert.equal(fromEther(await sdlToken.balanceOf(accounts[3])), 2000) }) }) diff --git a/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts index 515f03b5..07412abf 100644 --- a/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts +++ b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts @@ -77,7 +77,7 @@ describe('SDLPoolCCIPControllerSecondary', () => { await router.applyRampUpdates([[77, onRamp.address]], [], [[77, offRamp.address]]) - let boostController = await deploy('LinearBoostController', [4 * 365 * 86400, 4]) + let boostController = await deploy('LinearBoostController', [10, 4 * 365 * 86400, 4]) sdlPool = (await deployUpgradeable('SDLPoolSecondary', [ 'reSDL', 'reSDL', @@ -430,9 +430,13 @@ describe('SDLPoolCCIPControllerSecondary', () => { it('recoverTokens should work correctly', async () => { await linkToken.transfer(controller.address, toEther(1000)) await sdlToken.transfer(controller.address, toEther(2000)) - await controller.recoverTokens([linkToken.address, sdlToken.address], accounts[3]) + await controller.recoverTokens( + [linkToken.address, sdlToken.address], + [toEther(1000), toEther(2000)], + accounts[3] + ) - assert.equal(fromEther(await linkToken.balanceOf(accounts[3])), 1100) + assert.equal(fromEther(await linkToken.balanceOf(accounts[3])), 1000) assert.equal(fromEther(await sdlToken.balanceOf(accounts[3])), 2000) }) }) diff --git a/test/core/ccip/wrapped-token-bridge.test.ts b/test/core/ccip/wrapped-token-bridge.test.ts index aa9f1976..b5f5a693 100644 --- a/test/core/ccip/wrapped-token-bridge.test.ts +++ b/test/core/ccip/wrapped-token-bridge.test.ts @@ -102,10 +102,10 @@ describe('WrappedTokenBridge', () => { }) it('getFee should work correctly', async () => { - assert.equal(fromEther(await bridge.getFee(77, false)), 2) - assert.equal(fromEther(await bridge.getFee(77, true)), 3) - await expect(bridge.getFee(78, false)).to.be.reverted - await expect(bridge.getFee(78, true)).to.be.reverted + assert.equal(fromEther(await bridge.getFee(77, 1000, false)), 2) + assert.equal(fromEther(await bridge.getFee(77, 1000, true)), 3) + await expect(bridge.getFee(78, 1000, false)).to.be.reverted + await expect(bridge.getFee(78, 1000, true)).to.be.reverted }) it('transferTokens should work correctly with LINK fee', async () => { @@ -272,7 +272,11 @@ describe('WrappedTokenBridge', () => { it('recoverTokens should work correctly', async () => { await linkToken.transfer(bridge.address, toEther(1000)) await stakingPool.transfer(bridge.address, toEther(2000)) - await bridge.recoverTokens([linkToken.address, stakingPool.address], accounts[3]) + await bridge.recoverTokens( + [linkToken.address, stakingPool.address], + [toEther(1000), toEther(2000)], + accounts[3] + ) assert.equal(fromEther(await linkToken.balanceOf(accounts[3])), 1000) assert.equal(fromEther(await stakingPool.balanceOf(accounts[3])), 2000) diff --git a/test/core/sdlPool/linear-boost-controller.test.ts b/test/core/sdlPool/linear-boost-controller.test.ts index 5f58825b..b57c3deb 100644 --- a/test/core/sdlPool/linear-boost-controller.test.ts +++ b/test/core/sdlPool/linear-boost-controller.test.ts @@ -9,6 +9,7 @@ describe('LinearBoostController', () => { beforeEach(async () => { boostController = (await deploy('LinearBoostController', [ + 10, 4 * 365 * DAY, 4, ])) as LinearBoostController @@ -40,7 +41,10 @@ describe('LinearBoostController', () => { assert.equal(fromEther(await boostController.getBoostAmount(toEther(5), 2 * 365 * DAY)), 30) await expect(boostController.getBoostAmount(toEther(5), 2 * 365 * DAY + 1)).to.be.revertedWith( - 'MaxLockingDurationExceeded()' + 'InvalidLockingDuration()' + ) + await expect(boostController.getBoostAmount(toEther(5), 9)).to.be.revertedWith( + 'InvalidLockingDuration()' ) }) }) diff --git a/test/core/sdlPool/sdl-pool-primary.test.ts b/test/core/sdlPool/sdl-pool-primary.test.ts index 45d664b2..bae949f1 100644 --- a/test/core/sdlPool/sdl-pool-primary.test.ts +++ b/test/core/sdlPool/sdl-pool-primary.test.ts @@ -54,6 +54,7 @@ describe('SDLPoolPrimary', () => { await setupToken(sdlToken, accounts) boostController = (await deploy('LinearBoostController', [ + 10, 4 * 365 * DAY, 4, ])) as LinearBoostController @@ -1320,6 +1321,6 @@ describe('SDLPoolPrimary', () => { await expect( sdlPool.connect(signers[1]).transferFrom(accounts[1], accounts[0], 1) - ).to.be.revertedWith('TransferToCCIPController()') + ).to.be.revertedWith('TransferToInvalidAddress()') }) }) diff --git a/test/core/sdlPool/sdl-pool-secondary.test.ts b/test/core/sdlPool/sdl-pool-secondary.test.ts index 4fc4c3f1..74e3b5ef 100644 --- a/test/core/sdlPool/sdl-pool-secondary.test.ts +++ b/test/core/sdlPool/sdl-pool-secondary.test.ts @@ -86,6 +86,7 @@ describe('SDLPoolSecondary', () => { await setupToken(sdlToken, accounts) boostController = (await deploy('LinearBoostController', [ + 10, 4 * 365 * DAY, 4, ])) as LinearBoostController From f9391f9da633026c503e0ae759a2acee81b7f47f Mon Sep 17 00:00:00 2001 From: BkChoy Date: Tue, 20 Feb 2024 19:14:17 +1300 Subject: [PATCH 32/42] ccip stage 1 deployment script --- .../{ => ccip}/deploy-ccip-dest-tokens.ts | 2 +- scripts/prod/ccip/deploy-ccip-stage-1.ts | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) rename scripts/prod/{ => ccip}/deploy-ccip-dest-tokens.ts (92%) create mode 100644 scripts/prod/ccip/deploy-ccip-stage-1.ts diff --git a/scripts/prod/deploy-ccip-dest-tokens.ts b/scripts/prod/ccip/deploy-ccip-dest-tokens.ts similarity index 92% rename from scripts/prod/deploy-ccip-dest-tokens.ts rename to scripts/prod/ccip/deploy-ccip-dest-tokens.ts index d4c51d63..d1e0faff 100644 --- a/scripts/prod/deploy-ccip-dest-tokens.ts +++ b/scripts/prod/ccip/deploy-ccip-dest-tokens.ts @@ -1,4 +1,4 @@ -import { updateDeployments, deploy } from '../utils/deployment' +import { updateDeployments, deploy } from '../../utils/deployment' // SDL const sdl = { diff --git a/scripts/prod/ccip/deploy-ccip-stage-1.ts b/scripts/prod/ccip/deploy-ccip-stage-1.ts new file mode 100644 index 00000000..5f26a903 --- /dev/null +++ b/scripts/prod/ccip/deploy-ccip-stage-1.ts @@ -0,0 +1,37 @@ +import { WrappedTokenBridge } from '../../../typechain-types' +import { updateDeployments, deploy, getContract } from '../../utils/deployment' + +// should be deployed on primary chain (Ethereum Mainnet) + +const ccipRouter = '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D' +const multisigAddress = '0xB351EC0FEaF4B99FdFD36b484d9EC90D0422493D' + +async function main() { + const linkToken = await getContract('LINKToken') + const stLINKToken = await getContract('LINK_StakingPool') + const wstLINKToken = await getContract('LINK_WrappedSDToken') + + const wrappedTokenBridge = (await deploy('WrappedTokenBridge', [ + ccipRouter, + linkToken.address, + stLINKToken.address, + wstLINKToken.address, + ])) as WrappedTokenBridge + console.log('stLINK_WrappedTokenBridge deployed: ', wrappedTokenBridge.address) + + await (await wrappedTokenBridge.transferOwnership(multisigAddress)).wait() + + updateDeployments( + { + stLINK_WrappedTokenBridge: wrappedTokenBridge.address, + }, + { stLINK_wrappedTokenBridge: 'WrappedTokenBridge' } + ) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) From 3b6742a881be2624c00eef6995eee97d669f2c4b Mon Sep 17 00:00:00 2001 From: BkChoy Date: Thu, 22 Feb 2024 10:41:00 +1300 Subject: [PATCH 33/42] fix duplicate erc677 --- deployments/mainnet.json | 4 +- deployments/sepolia.json | 34 ---------- deployments/testnet.json | 90 --------------------------- scripts/test/deploy-test-contracts.ts | 4 +- scripts/test/setup-testnet.ts | 2 +- 5 files changed, 5 insertions(+), 129 deletions(-) delete mode 100644 deployments/sepolia.json delete mode 100644 deployments/testnet.json diff --git a/deployments/mainnet.json b/deployments/mainnet.json index 1eceb501..c55f3be3 100644 --- a/deployments/mainnet.json +++ b/deployments/mainnet.json @@ -1,11 +1,11 @@ { "LPLToken": { "address": "0x99295f1141d58a99e939f7be6bbe734916a875b8", - "artifact": "ERC677" + "artifact": "contracts/core/tokens/base/ERC677.sol:ERC677" }, "LINKToken": { "address": "0x514910771af9ca656af840dff83e8264ecf986ca", - "artifact": "ERC677" + "artifact": "contracts/core/tokens/base/ERC677.sol:ERC677" }, "SDLToken": { "address": "0xA95C5ebB86E0dE73B4fB8c47A45B792CFeA28C23", diff --git a/deployments/sepolia.json b/deployments/sepolia.json deleted file mode 100644 index 68a8f6ee..00000000 --- a/deployments/sepolia.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "SDLToken": { - "address": "0xD668b598e7F9d72b0D1B081c928eC8553503a188", - "artifact": "StakingAllowance" - }, - "LinearBoostController": { - "address": "0xCaEC3A1abD45Df880fD1B3252bdF29e5c4EE9eB7", - "artifact": "LinearBoostController" - }, - "SDLPool": { - "address": "0x31Fa516c6A602A1f7Fc4Ed0070Ee7Aea397cc4E7", - "artifact": "SDLPool" - }, - "LINKToken": { - "address": "0x34e754e54b286250fea278AeC99033Ae9e509e25", - "artifact": "ERC677" - }, - "LINK_StakingPool": { - "address": "0x71EC4a95e3C26280d7FFc52c4BfEc538325b676b", - "artifact": "StakingPool" - }, - "LINK_PriorityPool": { - "address": "0x6171927d7d982513af122C4Bc1C9d9E873e62273", - "artifact": "PriorityPool" - }, - "LINK_WrappedSDToken": { - "address": "0xE7a999ae5670B5b2B725A55d4b0E686B9e190826", - "artifact": "WrappedSDToken" - }, - "stLINK_SDLRewardsPool": { - "address": "0x0415A4374d1f16B79b3604DE13D0f15e58C0539d", - "artifact": "RewardsPoolWSD" - } -} diff --git a/deployments/testnet.json b/deployments/testnet.json deleted file mode 100644 index 0afb6f33..00000000 --- a/deployments/testnet.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "LPLToken": { - "address": "0x828CAe966B8Bf7c97B2e8189aa9A3c653ED72899", - "artifact": "ERC677" - }, - "LINKToken": { - "address": "0xCA2AaEE5c36BDd79CaC5493343878C30e38e9da6", - "artifact": "ERC677" - }, - "PoolOwnersV1": { - "address": "0x90012Cc1aB4D3234b3238A4300eE611a6a2D76C5", - "artifact": "PoolOwnersV1" - }, - "LINK_OwnersRewardsPoolV1": { - "address": "0xA395B90ccb0d4520b634B64Caeea9320DF276942", - "artifact": "OwnersRewardsPoolV1" - }, - "PoolAllowanceV1": { - "address": "0x5969a3D50b4cAd7145Bf3e8704323CdA165de497", - "artifact": "PoolAllowanceV1" - }, - "Multicall3": { - "address": "0xa5da925C02f03cc515d648b3D8441fB53DEA0a74", - "artifact": "Multicall3" - }, - "stETHToken": { - "address": "0xC1C9f268651a394AFbc021cc722175a84B8ae469", - "artifact": "ERC20" - }, - "rETHToken": { - "address": "0x8E555709107aA344E493AEA521B2e6C0b245fe16", - "artifact": "ERC20" - }, - "SDLToken": { - "address": "0x0a76496Bf29F130D9F2aA5eBc9f77A9B48850677", - "artifact": "StakingAllowance" - }, - "LPLMigration": { - "address": "0x4B929768c24A03f231776271c1AFb38cb5825992", - "artifact": "LPLMigration" - }, - "DelegatorPool": { - "address": "0xEA3ABd99869EdF0B8B929aCb5148472e188e84e9", - "artifact": "DelegatorPool" - }, - "PoolRouter": { - "address": "0x7eFcD4B32052d868611eEaB8E77b1d5fCec41880", - "artifact": "PoolRouter" - }, - "LINK_StakingPool": { - "address": "0xf189d68db38be09EEC9c6ab4bE36D72Ce49fab6a", - "artifact": "StakingPool" - }, - "LINK_WrappedSDToken": { - "address": "0x21B11E4309b1f56701A14EdC8abcFcD478Faa704", - "artifact": "WrappedSDToken" - }, - "stLINK_DelegatorRewardsPool": { - "address": "0x52C335Bf89EfAf84A4E80352022D39b465147D07", - "artifact": "RewardsPoolWSD" - }, - "GovernanceController": { - "address": "0xc2053111870caD9fC5473A43C9b9C4b6dF4E459A", - "artifact": "GovernanceController" - }, - "MerkleDistributor": { - "address": "0xC205E8Fe926Cc8f6dDF5F23A687984E8FedFbf01", - "artifact": "MerkleDistributor" - }, - "ixETH_WrappedSDToken": { - "address": "0xfF15126089cb86Ad8C316648a1B7cEE5ce83a0Bd", - "artifact": "WrappedSDToken" - }, - "ETH_LiquidSDIndexPool": { - "address": "0xe4415600d458AB82853aEB0FE4ac0d0a9285Cb1A", - "artifact": "LiquidSDIndexPool" - }, - "StakedotlinkCouncil": { - "address": "0xBBa6B47203FcaF5e24A789Eb0c7804505B2DeE58", - "artifact": "StakedotlinkCouncil" - }, - "cbETHToken": { - "address": "0x3467A653237a56E8c20d730cdb5a8FB20034E7b7", - "artifact": "ERC20" - }, - "sfrxETHToken": { - "address": "0x57c13De15Af883052b09f02ffF76E12d56dEC673", - "artifact": "ERC20" - } -} diff --git a/scripts/test/deploy-test-contracts.ts b/scripts/test/deploy-test-contracts.ts index 6369d0a0..0e22007c 100644 --- a/scripts/test/deploy-test-contracts.ts +++ b/scripts/test/deploy-test-contracts.ts @@ -83,8 +83,8 @@ async function main() { sfrxETHToken: sfrxETHToken.address, }, { - LPLToken: 'ERC677', - LINKToken: 'ERC677', + LPLToken: 'contracts/core/tokens/base/ERC677.sol:ERC677', + LINKToken: 'contracts/core/tokens/base/ERC677.sol:ERC677', LINK_OwnersRewardsPoolV1: 'OwnersRewardsPoolV1', stETHToken: 'ERC20', rETHToken: 'ERC20', diff --git a/scripts/test/setup-testnet.ts b/scripts/test/setup-testnet.ts index 11b92e6f..d8473e23 100644 --- a/scripts/test/setup-testnet.ts +++ b/scripts/test/setup-testnet.ts @@ -123,7 +123,7 @@ async function main() { }, { SDLToken: 'StakingAllowance', - LINKToken: 'ERC677', + LINKToken: 'contracts/core/tokens/base/ERC677.sol:ERC677', LINK_StakingPool: 'StakingPool', LINK_PriorityPool: 'PriorityPool', LINK_WrappedSDToken: 'WrappedSDToken', From fac4ed0d671354e97f04047946f04ac908ce2f05 Mon Sep 17 00:00:00 2001 From: Jonny Date: Thu, 22 Feb 2024 10:18:58 +0000 Subject: [PATCH 34/42] Deploy wrapped token bridge --- deployments/mainnet.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deployments/mainnet.json b/deployments/mainnet.json index c55f3be3..605aaeaf 100644 --- a/deployments/mainnet.json +++ b/deployments/mainnet.json @@ -170,5 +170,9 @@ "LINK_PP_DistributionOracle": { "address": "0x2285AC429cCCAaE7cC1E27BfBe617bC626B443CF", "artifact": "DistributionOracle" + }, + "stLINK_WrappedTokenBridge": { + "address": "0x6C1E2D2c55C83De945e3f37dF694cdE8452C1E82", + "artifact": "stLINK_WrappedTokenBridge" } } From 212db4fc564f17f240d6681d24d750d94a2bda98 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Fri, 23 Feb 2024 10:49:25 +1300 Subject: [PATCH 35/42] fixed deployment name --- deployments/mainnet.json | 2 +- scripts/prod/ccip/deploy-ccip-stage-1.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deployments/mainnet.json b/deployments/mainnet.json index 605aaeaf..a082f1c1 100644 --- a/deployments/mainnet.json +++ b/deployments/mainnet.json @@ -173,6 +173,6 @@ }, "stLINK_WrappedTokenBridge": { "address": "0x6C1E2D2c55C83De945e3f37dF694cdE8452C1E82", - "artifact": "stLINK_WrappedTokenBridge" + "artifact": "WrappedTokenBridge" } } diff --git a/scripts/prod/ccip/deploy-ccip-stage-1.ts b/scripts/prod/ccip/deploy-ccip-stage-1.ts index 5f26a903..e53582f0 100644 --- a/scripts/prod/ccip/deploy-ccip-stage-1.ts +++ b/scripts/prod/ccip/deploy-ccip-stage-1.ts @@ -25,7 +25,7 @@ async function main() { { stLINK_WrappedTokenBridge: wrappedTokenBridge.address, }, - { stLINK_wrappedTokenBridge: 'WrappedTokenBridge' } + { stLINK_WrappedTokenBridge: 'WrappedTokenBridge' } ) } From 8acfd335a701ff1f0356692f3241a398f8b07e3d Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 26 Feb 2024 09:34:20 +1300 Subject: [PATCH 36/42] removed onlyOwner --- contracts/core/ccip/WrappedTokenBridge.sol | 2 +- contracts/core/sdlPool/SDLPoolSecondary.sol | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/contracts/core/ccip/WrappedTokenBridge.sol b/contracts/core/ccip/WrappedTokenBridge.sol index 4169e85e..a150b2e2 100644 --- a/contracts/core/ccip/WrappedTokenBridge.sol +++ b/contracts/core/ccip/WrappedTokenBridge.sol @@ -108,7 +108,7 @@ contract WrappedTokenBridge is CCIPReceiver { uint256 _amount, bool _payNative, uint256 _maxLINKFee - ) external payable onlyOwner returns (bytes32 messageId) { + ) external payable returns (bytes32 messageId) { if (_payNative == false && msg.value != 0) revert InvalidMsgValue(); token.safeTransferFrom(msg.sender, address(this), _amount); diff --git a/contracts/core/sdlPool/SDLPoolSecondary.sol b/contracts/core/sdlPool/SDLPoolSecondary.sol index 179a102c..fe897b73 100644 --- a/contracts/core/sdlPool/SDLPoolSecondary.sol +++ b/contracts/core/sdlPool/SDLPoolSecondary.sol @@ -28,7 +28,7 @@ contract SDLPoolSecondary is SDLPool { mapping(address => NewLockPointer[]) internal newLocksByOwner; uint128 public updateBatchIndex; - uint64 public updateInProgress; + uint64 internal updateInProgress; uint64 internal updateNeeded; int256 public queuedRESDLSupplyChange; @@ -356,6 +356,14 @@ contract SDLPoolSecondary is SDLPool { return updateNeeded == 1 && updateInProgress == 0; } + /** + * @notice returns whether an update is in progress + * @return whether update is in progress + **/ + function isUpdateInProgress() external view returns (bool) { + return updateInProgress == 1; + } + /** * @notice queues a new lock to be minted * @param _owner owner of lock From b45c7f513d2482642064f00e275a33267dbc7d22 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 26 Feb 2024 10:08:36 +1300 Subject: [PATCH 37/42] fixed tests --- contracts/core/ccip/base/SDLPoolCCIPController.sol | 4 ++++ test/core/sdlPool/sdl-pool-secondary.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/core/ccip/base/SDLPoolCCIPController.sol b/contracts/core/ccip/base/SDLPoolCCIPController.sol index 668f9c12..62a28355 100644 --- a/contracts/core/ccip/base/SDLPoolCCIPController.sol +++ b/contracts/core/ccip/base/SDLPoolCCIPController.sol @@ -102,6 +102,10 @@ abstract contract SDLPoolCCIPController is CCIPReceiver { } } + /** + * @notice Processes a received message + * @param _message CCIP message + **/ function ccipReceive(Client.Any2EVMMessage calldata _message) external override onlyRouter { _verifyCCIPSender(_message); diff --git a/test/core/sdlPool/sdl-pool-secondary.test.ts b/test/core/sdlPool/sdl-pool-secondary.test.ts index 74e3b5ef..35dbacab 100644 --- a/test/core/sdlPool/sdl-pool-secondary.test.ts +++ b/test/core/sdlPool/sdl-pool-secondary.test.ts @@ -1500,7 +1500,7 @@ describe('SDLPoolSecondary', () => { [2, 300] ) await sdlPool.handleOutgoingUpdate() - assert.equal((await sdlPool.updateInProgress()).toNumber(), 1) + assert.equal(await sdlPool.isUpdateInProgress(), true) assert.equal(await sdlPool.shouldUpdate(), false) assert.equal((await sdlPool.updateBatchIndex()).toNumber(), 2) assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0) @@ -1558,7 +1558,7 @@ describe('SDLPoolSecondary', () => { [0, -50] ) await sdlPool.handleOutgoingUpdate() - assert.equal((await sdlPool.updateInProgress()).toNumber(), 1) + assert.equal(await sdlPool.isUpdateInProgress(), true) assert.equal(await sdlPool.shouldUpdate(), false) assert.equal((await sdlPool.updateBatchIndex()).toNumber(), 3) assert.equal(fromEther(await sdlPool.queuedRESDLSupplyChange()), 0) From fd98f7b96754431f077da27d2458bb1395dc0eb8 Mon Sep 17 00:00:00 2001 From: AnonJon Date: Mon, 26 Feb 2024 11:14:34 -0700 Subject: [PATCH 38/42] update path --- contracts/linkStaking/CommunityVaultAutomation.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/linkStaking/CommunityVaultAutomation.sol b/contracts/linkStaking/CommunityVaultAutomation.sol index 469f107b..07d3621c 100644 --- a/contracts/linkStaking/CommunityVaultAutomation.sol +++ b/contracts/linkStaking/CommunityVaultAutomation.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.15; import {CommunityVCS} from "./CommunityVCS.sol"; import {IVault} from "./interfaces/IVault.sol"; -import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/interfaces/AutomationCompatibleInterface.sol"; +import {AutomationCompatibleInterface} from "@chainlink/contracts/src/v0.8/automation/interfaces/AutomationCompatibleInterface.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; contract CommunityVaultAutomation is AutomationCompatibleInterface, Ownable { From e9e9670100d88bb478a96b081448d293e9a70770 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Tue, 27 Feb 2024 10:42:52 +1300 Subject: [PATCH 39/42] add missing comment --- contracts/core/ccip/base/SDLPoolCCIPController.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/core/ccip/base/SDLPoolCCIPController.sol b/contracts/core/ccip/base/SDLPoolCCIPController.sol index 62a28355..2b71767f 100644 --- a/contracts/core/ccip/base/SDLPoolCCIPController.sol +++ b/contracts/core/ccip/base/SDLPoolCCIPController.sol @@ -89,6 +89,11 @@ abstract contract SDLPoolCCIPController is CCIPReceiver { ISDLPool.RESDLToken calldata _reSDLToken ) external virtual; + /** + * @notice Sends a CCIP message + * @param _destinationChainSelector id of destination chain + * @param _evmToAnyMessage CCIP message + **/ function ccipSend(uint64 _destinationChainSelector, Client.EVM2AnyMessage calldata _evmToAnyMessage) external payable From 9ab52b273bb809e4bd566c6673367abb3d0b7353 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Thu, 29 Feb 2024 14:52:31 +1300 Subject: [PATCH 40/42] use dynamic gas limit for ccip messaging --- contracts/core/RewardsInitiator.sol | 9 +- contracts/core/ccip/RESDLTokenBridge.sol | 35 ++-- .../ccip/SDLPoolCCIPControllerPrimary.sol | 127 ++++++++------- .../ccip/SDLPoolCCIPControllerSecondary.sol | 78 +++++---- .../ISDLPoolCCIPControllerPrimary.sol | 2 +- .../core/test/SDLPoolCCIPControllerMock.sol | 2 +- test/core/ccip/resdl-token-bridge.test.ts | 57 +++---- .../sdl-pool-ccip-controller-primary.test.ts | 149 +++++++++++++----- ...sdl-pool-ccip-controller-secondary.test.ts | 106 ++++--------- test/core/rewards-initiator.test.ts | 4 +- 10 files changed, 314 insertions(+), 255 deletions(-) diff --git a/contracts/core/RewardsInitiator.sol b/contracts/core/RewardsInitiator.sol index 13e181b3..d69f3d57 100644 --- a/contracts/core/RewardsInitiator.sol +++ b/contracts/core/RewardsInitiator.sol @@ -34,11 +34,16 @@ contract RewardsInitiator is Ownable { * @notice updates strategy rewards in the staking pool and distributes rewards to cross-chain SDL pools * @param _strategyIdxs indexes of strategies to update rewards for * @param _data encoded data to be passed to each strategy + * @param _gasLimits list of gas limits to use for CCIP messages on secondary chains **/ - function updateRewards(uint256[] calldata _strategyIdxs, bytes calldata _data) external { + function updateRewards( + uint256[] calldata _strategyIdxs, + bytes calldata _data, + uint256[] calldata _gasLimits + ) external { if (!whitelistedCallers[msg.sender]) revert SenderNotAuthorized(); stakingPool.updateStrategyRewards(_strategyIdxs, _data); - sdlPoolCCIPController.distributeRewards(); + sdlPoolCCIPController.distributeRewards(_gasLimits); } /** diff --git a/contracts/core/ccip/RESDLTokenBridge.sol b/contracts/core/ccip/RESDLTokenBridge.sol index e95e8dd3..098d46b3 100644 --- a/contracts/core/ccip/RESDLTokenBridge.sol +++ b/contracts/core/ccip/RESDLTokenBridge.sol @@ -22,8 +22,6 @@ contract RESDLTokenBridge is Ownable { ISDLPool public sdlPool; ISDLPoolCCIPController public sdlPoolCCIPController; - mapping(uint64 => bytes) public extraArgsByChain; - event TokenTransferred( bytes32 indexed messageId, uint64 indexed destinationChainSelector, @@ -40,7 +38,6 @@ contract RESDLTokenBridge is Ownable { address receiver, uint256 tokenId ); - event SetExtraArgs(uint64 indexed chainSelector, bytes extraArgs); error InsufficientFee(); error TransferFailed(); @@ -80,13 +77,15 @@ contract RESDLTokenBridge is Ownable { * @param _tokenId id of reSDL token * @param _payNative whether fee should be paid natively or with LINK * @param _maxLINKFee call will revert if LINK fee exceeds this value + * @param _gasLimit gas limit to use for CCIP message on destination chain **/ function transferRESDL( uint64 _destinationChainSelector, address _receiver, uint256 _tokenId, bool _payNative, - uint256 _maxLINKFee + uint256 _maxLINKFee, + uint256 _gasLimit ) external payable returns (bytes32 messageId) { if (msg.sender != sdlPool.ownerOf(_tokenId)) revert SenderNotAuthorized(); if (_receiver == address(0)) revert InvalidReceiver(); @@ -97,7 +96,6 @@ contract RESDLTokenBridge is Ownable { msg.sender, _tokenId ); - bytes memory extraArgs = extraArgsByChain[_destinationChainSelector]; Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( _receiver, @@ -105,7 +103,7 @@ contract RESDLTokenBridge is Ownable { reSDLToken, destination, _payNative ? address(0) : address(linkToken), - extraArgs + _gasLimit ); uint256 fees = IRouterClient(sdlPoolCCIPController.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); @@ -138,31 +136,26 @@ contract RESDLTokenBridge is Ownable { * @notice Returns the current fee for an reSDL transfer * @param _destinationChainSelector id of destination chain * @param _payNative whether fee should be paid natively or with LINK + * @param _gasLimit gas limit to use for CCIP message on destination chain * @return fee current fee **/ - function getFee(uint64 _destinationChainSelector, bool _payNative) external view returns (uint256) { + function getFee( + uint64 _destinationChainSelector, + bool _payNative, + uint256 _gasLimit + ) external view returns (uint256) { Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( address(this), 0, ISDLPool.RESDLToken(0, 0, 0, 0, 0), address(this), _payNative ? address(0) : address(linkToken), - extraArgsByChain[_destinationChainSelector] + _gasLimit ); return IRouterClient(sdlPoolCCIPController.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); } - /** - * @notice Sets the extra args used for sending reSDL to a chain - * @param _chainSelector id of chain - * @param _extraArgs extra args as defined in CCIP API - **/ - function setExtraArgs(uint64 _chainSelector, bytes calldata _extraArgs) external onlyOwner { - extraArgsByChain[_chainSelector] = _extraArgs; - emit SetExtraArgs(_chainSelector, _extraArgs); - } - /** * @notice Processes a received message * @dev handles incoming reSDL transfers @@ -199,7 +192,7 @@ contract RESDLTokenBridge is Ownable { * @param _reSDLToken reSDL token * @param _destination address of destination contract * @param _feeTokenAddress address of token that fees will be paid in - * @param _extraArgs encoded args as defined in CCIP API + * @param _gasLimit gas limit to use for CCIP message on destination chain **/ function _buildCCIPMessage( address _receiver, @@ -207,7 +200,7 @@ contract RESDLTokenBridge is Ownable { ISDLPool.RESDLToken memory _reSDLToken, address _destination, address _feeTokenAddress, - bytes memory _extraArgs + uint256 _gasLimit ) internal view returns (Client.EVM2AnyMessage memory) { Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1); Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({ @@ -228,7 +221,7 @@ contract RESDLTokenBridge is Ownable { _reSDLToken.expiry ), tokenAmounts: tokenAmounts, - extraArgs: _extraArgs, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: _gasLimit})), feeToken: _feeTokenAddress }); diff --git a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol index e5dd5e66..ce408190 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol @@ -16,24 +16,29 @@ interface ISDLPoolPrimary is ISDLPool { contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { using SafeERC20 for IERC20; + struct QueuedUpdate { + uint64 chainSelector; + uint192 mintStartIndex; + } + uint64[] internal whitelistedChains; mapping(uint64 => address) public whitelistedDestinations; - - mapping(uint64 => bytes) public updateExtraArgsByChain; - mapping(uint64 => bytes) public rewardsExtraArgsByChain; mapping(uint64 => uint256) public reSDLSupplyByChain; mapping(address => address) public wrappedRewardTokens; address public rewardsInitiator; + address public updateInitiator; + + QueuedUpdate[] internal queuedUpdates; event DistributeRewards(bytes32 indexed messageId, uint64 indexed destinationChainSelector, uint256 fees); - event ChainAdded(uint64 indexed chainSelector, address destination, bytes updateExtraArgs, bytes rewardsExtraArgs); + event ChainAdded(uint64 indexed chainSelector, address destination); event ChainRemoved(uint64 indexed chainSelector, address destination); - event SetUpdateExtraArgs(uint64 indexed chainSelector, bytes extraArgs); - event SetRewardsExtraArgs(uint64 indexed chainSelector, bytes extraArgs); event SetWrappedRewardToken(address indexed token, address rewardToken); + error InvalidLength(); + /** * @notice Initializes the contract * @param _router address of the CCIP router @@ -41,24 +46,34 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { * @param _sdlToken address of the SDL token * @param _sdlPool address of the SDL Pool * @param _maxLINKFee max fee to be paid on an outgoing message + * @param _updateInitiator address of the update initiator **/ constructor( address _router, address _linkToken, address _sdlToken, address _sdlPool, - uint256 _maxLINKFee - ) SDLPoolCCIPController(_router, _linkToken, _sdlToken, _sdlPool, _maxLINKFee) {} + uint256 _maxLINKFee, + address _updateInitiator + ) SDLPoolCCIPController(_router, _linkToken, _sdlToken, _sdlPool, _maxLINKFee) { + updateInitiator = _updateInitiator; + } modifier onlyRewardsInitiator() { if (msg.sender != rewardsInitiator) revert SenderNotAuthorized(); _; } + modifier onlyUpdateInitiator() { + if (msg.sender != updateInitiator) revert SenderNotAuthorized(); + _; + } + /** * @notice Claims and distributes rewards between all secondary chains + * @param _gasLimits list of gas limits to use for CCIP messages on secondary chains **/ - function distributeRewards() external onlyRewardsInitiator { + function distributeRewards(uint256[] calldata _gasLimits) external onlyRewardsInitiator { uint256 totalRESDL = ISDLPoolPrimary(sdlPool).effectiveBalanceOf(address(this)); address[] memory tokens = ISDLPoolPrimary(sdlPool).supportedTokens(); uint256 numDestinations = whitelistedChains.length; @@ -93,8 +108,23 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { } for (uint256 i = 0; i < numDestinations; ++i) { - _distributeRewards(whitelistedChains[i], tokens, distributionAmounts[i]); + _distributeRewards(whitelistedChains[i], tokens, distributionAmounts[i], _gasLimits[i]); + } + } + + /** + * @notice Executes all queued updates + * @param _gasLimits list of gas limits to use for CCIP messages on secondary chains + **/ + function executeQueuedUpdates(uint256[] calldata _gasLimits) external onlyUpdateInitiator { + if (_gasLimits.length == 0 || _gasLimits.length != queuedUpdates.length) revert InvalidLength(); + + for (uint256 i = 0; i < _gasLimits.length; ++i) { + QueuedUpdate memory update = queuedUpdates[i]; + _ccipSendUpdate(update.chainSelector, update.mintStartIndex, _gasLimits[i]); } + + delete queuedUpdates; } /** @@ -146,26 +176,25 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { return whitelistedChains; } + /** + * @notice Returns a list of all queued updates + * @return list of queued updates + **/ + function getQueuedUpdates() external view returns (QueuedUpdate[] memory) { + return queuedUpdates; + } + /** * @notice Whitelists a new chain * @param _chainSelector id of chain * @param _destination address to receive CCIP messages on chain - * @param _updateExtraArgs extraArgs for sending updates to this destination as defined in CCIP docs - * @param _rewardsExtraArgs extraArgs for sending rewards to this destination as defined in CCIP docs **/ - function addWhitelistedChain( - uint64 _chainSelector, - address _destination, - bytes calldata _updateExtraArgs, - bytes calldata _rewardsExtraArgs - ) external onlyOwner { + function addWhitelistedChain(uint64 _chainSelector, address _destination) external onlyOwner { if (whitelistedDestinations[_chainSelector] != address(0)) revert AlreadyAdded(); if (_destination == address(0)) revert InvalidDestination(); whitelistedChains.push(_chainSelector); whitelistedDestinations[_chainSelector] = _destination; - updateExtraArgsByChain[_chainSelector] = _updateExtraArgs; - rewardsExtraArgsByChain[_chainSelector] = _rewardsExtraArgs; - emit ChainAdded(_chainSelector, _destination, _updateExtraArgs, _rewardsExtraArgs); + emit ChainAdded(_chainSelector, _destination); } /** @@ -184,8 +213,6 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { } delete whitelistedDestinations[_chainSelector]; - delete updateExtraArgsByChain[_chainSelector]; - delete rewardsExtraArgsByChain[_chainSelector]; } /** @@ -209,28 +236,6 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { emit SetWrappedRewardToken(_token, _wrappedToken); } - /** - * @notice Sets the extra args used for sending updates to a chain - * @param _chainSelector id of chain - * @param _updateExtraArgs extra args as defined in CCIP API - **/ - function setUpdateExtraArgs(uint64 _chainSelector, bytes calldata _updateExtraArgs) external onlyOwner { - if (whitelistedDestinations[_chainSelector] == address(0)) revert InvalidDestination(); - updateExtraArgsByChain[_chainSelector] = _updateExtraArgs; - emit SetUpdateExtraArgs(_chainSelector, _updateExtraArgs); - } - - /** - * @notice Sets the extra args used for sending rewards to a chain - * @param _chainSelector id of chain - * @param _rewardsExtraArgs extra args as defined in CCIP API - **/ - function setRewardsExtraArgs(uint64 _chainSelector, bytes calldata _rewardsExtraArgs) external onlyOwner { - if (whitelistedDestinations[_chainSelector] == address(0)) revert InvalidDestination(); - rewardsExtraArgsByChain[_chainSelector] = _rewardsExtraArgs; - emit SetRewardsExtraArgs(_chainSelector, _rewardsExtraArgs); - } - /** * @notice Sets the rewards initiator * @dev this address has sole authority to update rewards @@ -240,16 +245,27 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { rewardsInitiator = _rewardsInitiator; } + /** + * @notice Sets the update initiator + * @dev this address has sole authority to send update responses to secondary chains + * @param _updateInitiator address of update initiator + **/ + function setUpdateInitiator(address _updateInitiator) external onlyOwner { + updateInitiator = _updateInitiator; + } + /** * @notice Distributes rewards to a single chain * @param _destinationChainSelector id of chain * @param _rewardTokens list of reward tokens to distribute * @param _rewardTokenAmounts list of reward token amounts to distribute + * @param _gasLimit gas limit to use for CCIP message on destination chain **/ function _distributeRewards( uint64 _destinationChainSelector, address[] memory _rewardTokens, - uint256[] memory _rewardTokenAmounts + uint256[] memory _rewardTokenAmounts, + uint256 _gasLimit ) internal { address destination = whitelistedDestinations[_destinationChainSelector]; if (destination == address(0)) revert InvalidDestination(); @@ -279,7 +295,7 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { 0, rewardTokens, rewardTokenAmounts, - rewardsExtraArgsByChain[_destinationChainSelector] + _gasLimit ); IRouterClient router = IRouterClient(this.getRouter()); @@ -309,7 +325,7 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { uint256 mintStartIndex = ISDLPoolPrimary(sdlPool).handleIncomingUpdate(numNewRESDLTokens, totalRESDLSupplyChange); - _ccipSendUpdate(sourceChainSelector, mintStartIndex); + queuedUpdates.push(QueuedUpdate(sourceChainSelector, uint192(mintStartIndex))); emit MessageReceived(_message.messageId, sourceChainSelector); } @@ -318,14 +334,19 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { * @notice Sends an update to a secondary chain * @param _destinationChainSelector id of destination chain * @param _mintStartIndex first index to be used for minting new reSDL tokens + * @param _gasLimit gas limit to use for CCIP message on destination chain **/ - function _ccipSendUpdate(uint64 _destinationChainSelector, uint256 _mintStartIndex) internal { + function _ccipSendUpdate( + uint64 _destinationChainSelector, + uint256 _mintStartIndex, + uint256 _gasLimit + ) internal { Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( whitelistedDestinations[_destinationChainSelector], _mintStartIndex, new address[](0), new uint256[](0), - updateExtraArgsByChain[_destinationChainSelector] + _gasLimit ); IRouterClient router = IRouterClient(this.getRouter()); @@ -344,14 +365,14 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { * @param _mintStartIndex first index to be used for minting new reSDL tokens * @param _tokens list of tokens to transfer * @param _tokenAmounts list of token amounts to transfer - * @param _extraArgs encoded args as defined in CCIP API + * @param _gasLimit gas limit to use for CCIP message on destination chain **/ function _buildCCIPMessage( address _destination, uint256 _mintStartIndex, address[] memory _tokens, uint256[] memory _tokenAmounts, - bytes memory _extraArgs + uint256 _gasLimit ) internal view returns (Client.EVM2AnyMessage memory) { bool isRewardDistribution = _tokens.length != 0; @@ -364,7 +385,7 @@ contract SDLPoolCCIPControllerPrimary is SDLPoolCCIPController { receiver: abi.encode(_destination), data: isRewardDistribution ? bytes("") : abi.encode(_mintStartIndex), tokenAmounts: tokenAmounts, - extraArgs: _extraArgs, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: _gasLimit})), feeToken: address(linkToken) }); diff --git a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol index cdf32e89..04bb6f11 100644 --- a/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol +++ b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol @@ -20,13 +20,13 @@ interface ISDLPoolSecondary is ISDLPool { contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { using SafeERC20 for IERC20; - uint64 public immutable primaryChainSelector; - address public immutable primaryChainDestination; - bytes public extraArgs; + address public updateInitiator; - bool public shouldUpdate; + uint64 timeOfLastUpdate; + uint64 minTimeBetweenUpdates; - event SetExtraArgs(bytes extraArgs); + uint64 public immutable primaryChainSelector; + address public immutable primaryChainDestination; error UpdateConditionsNotMet(); @@ -39,7 +39,8 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { * @param _primaryChainSelector id of the primary chain * @param _primaryChainDestination address to receive messages on primary chain * @param _maxLINKFee max fee to be paid on an outgoing message - * @param _extraArgs extra args as defined in CCIP API to be used for outgoing messages + * @param _updateInitiator address of the update initiator + * @param _minTimeBetweenUpdates min time between updates **/ constructor( address _router, @@ -49,31 +50,37 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { uint64 _primaryChainSelector, address _primaryChainDestination, uint256 _maxLINKFee, - bytes memory _extraArgs + address _updateInitiator, + uint64 _minTimeBetweenUpdates ) SDLPoolCCIPController(_router, _linkToken, _sdlToken, _sdlPool, _maxLINKFee) { primaryChainSelector = _primaryChainSelector; primaryChainDestination = _primaryChainDestination; - extraArgs = _extraArgs; + updateInitiator = _updateInitiator; + minTimeBetweenUpdates = _minTimeBetweenUpdates; + } + + modifier onlyUpdateInitiator() { + if (msg.sender != updateInitiator) revert SenderNotAuthorized(); + _; } /** - * @notice Returns whether an update to the primary chain should be initiated - * @dev used by Chainlink automation - * @return whether an update should be initiated + * @notice Executes an update to the primary chain if update conditions are met + * @param _gasLimit gas limit to use for CCIP message on destination chain **/ - function checkUpkeep(bytes calldata) external view returns (bool, bytes memory) { - return (shouldUpdate, "0x"); + function executeUpdate(uint256 _gasLimit) external onlyUpdateInitiator { + if (!shouldUpdate()) revert UpdateConditionsNotMet(); + + timeOfLastUpdate = uint64(block.timestamp); + _initiateUpdate(primaryChainSelector, primaryChainDestination, _gasLimit); } /** - * @notice Initiates an update to the primary chain if update conditions are met - * @dev used by Chainlink automation + * @notice Returns whether an update should be sent to the primary chain + * @return whether update should be sent **/ - function performUpkeep(bytes calldata) external { - if (!shouldUpdate) revert UpdateConditionsNotMet(); - - shouldUpdate = false; - _initiateUpdate(primaryChainSelector, primaryChainDestination, extraArgs); + function shouldUpdate() public view returns (bool) { + return ISDLPoolSecondary(sdlPool).shouldUpdate() && block.timestamp > timeOfLastUpdate + minTimeBetweenUpdates; } /** @@ -110,24 +117,32 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { } /** - * @notice Sets the extra args for sending updates to the primary chain - * @param _extraArgs extra args as defined in CCIP API + * @notice Sets the update initiator + * @dev this address has sole authority to send updates to the primary chain + * @param _updateInitiator address of update initiator + **/ + function setUpdateInitiator(address _updateInitiator) external onlyOwner { + updateInitiator = _updateInitiator; + } + + /** + * @notice Sets the minimum time between sending updates to the primary chain + * @param _minTimeBetweenUpdates min time in seconds **/ - function setExtraArgs(bytes calldata _extraArgs) external onlyOwner { - extraArgs = _extraArgs; - emit SetExtraArgs(_extraArgs); + function setMinTimeBetweenUpdates(uint64 _minTimeBetweenUpdates) external onlyOwner { + minTimeBetweenUpdates = _minTimeBetweenUpdates; } /** * @notice Initiates an update to the primary chain * @param _destinationChainSelector id of destination chain * @param _destination address to receive message on destination chain - * @param _extraArgs extra args as defined in CCIP API + * @param _gasLimit gas limit to use for CCIP message on destination chain **/ function _initiateUpdate( uint64 _destinationChainSelector, address _destination, - bytes memory _extraArgs + uint256 _gasLimit ) internal { (uint256 numNewRESDLTokens, int256 totalRESDLSupplyChange) = ISDLPoolSecondary(sdlPool).handleOutgoingUpdate(); @@ -135,7 +150,7 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { _destination, numNewRESDLTokens, totalRESDLSupplyChange, - _extraArgs + _gasLimit ); IRouterClient router = IRouterClient(this.getRouter()); @@ -162,7 +177,6 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { IERC20(rewardTokens[i]).safeTransfer(sdlPool, _message.destTokenAmounts[i].amount); } ISDLPoolSecondary(sdlPool).distributeTokens(rewardTokens); - if (ISDLPoolSecondary(sdlPool).shouldUpdate()) shouldUpdate = true; } } else { uint256 mintStartIndex = abi.decode(_message.data, (uint256)); @@ -178,19 +192,19 @@ contract SDLPoolCCIPControllerSecondary is SDLPoolCCIPController { * @param _destination address of destination contract * @param _numNewRESDLTokens number of new reSDL NFTs to be minted * @param _totalRESDLSupplyChange reSDL supply change since last update - * @param _extraArgs encoded args as defined in CCIP API + * @param _gasLimit gas limit to use for CCIP message on destination chain **/ function _buildCCIPMessage( address _destination, uint256 _numNewRESDLTokens, int256 _totalRESDLSupplyChange, - bytes memory _extraArgs + uint256 _gasLimit ) internal view returns (Client.EVM2AnyMessage memory) { Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({ receiver: abi.encode(_destination), data: abi.encode(_numNewRESDLTokens, _totalRESDLSupplyChange), tokenAmounts: new Client.EVMTokenAmount[](0), - extraArgs: _extraArgs, + extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: _gasLimit})), feeToken: address(linkToken) }); diff --git a/contracts/core/interfaces/ISDLPoolCCIPControllerPrimary.sol b/contracts/core/interfaces/ISDLPoolCCIPControllerPrimary.sol index f33bb98a..54a95fb5 100644 --- a/contracts/core/interfaces/ISDLPoolCCIPControllerPrimary.sol +++ b/contracts/core/interfaces/ISDLPoolCCIPControllerPrimary.sol @@ -4,5 +4,5 @@ pragma solidity 0.8.15; import "./ISDLPoolCCIPController.sol"; interface ISDLPoolCCIPControllerPrimary is ISDLPoolCCIPController { - function distributeRewards() external; + function distributeRewards(uint256[] calldata _gasLimits) external; } diff --git a/contracts/core/test/SDLPoolCCIPControllerMock.sol b/contracts/core/test/SDLPoolCCIPControllerMock.sol index 24883909..c213caef 100644 --- a/contracts/core/test/SDLPoolCCIPControllerMock.sol +++ b/contracts/core/test/SDLPoolCCIPControllerMock.sol @@ -44,7 +44,7 @@ contract SDLPoolCCIPControllerMock { sdlPool.handleIncomingRESDL(_receiver, _tokenId, _reSDLToken); } - function distributeRewards() external { + function distributeRewards(uint256[] calldata) external { rewardsDistributed++; } diff --git a/test/core/ccip/resdl-token-bridge.test.ts b/test/core/ccip/resdl-token-bridge.test.ts index 95a434f3..38ba73e3 100644 --- a/test/core/ccip/resdl-token-bridge.test.ts +++ b/test/core/ccip/resdl-token-bridge.test.ts @@ -81,6 +81,7 @@ describe('RESDLTokenBridge', () => { sdlToken.address, sdlPool.address, toEther(10), + accounts[0], ])) as SDLPoolCCIPControllerPrimary bridge = (await deploy('RESDLTokenBridge', [ @@ -93,8 +94,7 @@ describe('RESDLTokenBridge', () => { await sdlPoolCCIPController.setRESDLTokenBridge(bridge.address) await sdlPool.setCCIPController(sdlPoolCCIPController.address) await linkToken.approve(bridge.address, ethers.constants.MaxUint256) - await bridge.setExtraArgs(77, '0x11') - await sdlPoolCCIPController.addWhitelistedChain(77, accounts[6], '0x', '0x') + await sdlPoolCCIPController.addWhitelistedChain(77, accounts[6]) await sdlToken.transfer(accounts[1], toEther(200)) await sdlToken.transferAndCall( @@ -110,10 +110,10 @@ describe('RESDLTokenBridge', () => { }) it('getFee should work correctly', async () => { - assert.equal(fromEther(await bridge.getFee(77, false)), 2) - assert.equal(fromEther(await bridge.getFee(77, true)), 3) - await expect(bridge.getFee(78, false)).to.be.reverted - await expect(bridge.getFee(78, true)).to.be.reverted + assert.equal(fromEther(await bridge.getFee(77, false, 1)), 2) + assert.equal(fromEther(await bridge.getFee(77, true, 1)), 3) + await expect(bridge.getFee(78, false, 1)).to.be.reverted + await expect(bridge.getFee(78, true, 1)).to.be.reverted }) it('transferRESDL should work correctly with LINK fee', async () => { @@ -124,7 +124,7 @@ describe('RESDLTokenBridge', () => { let preFeeBalance = await linkToken.balanceOf(accounts[0]) - await bridge.transferRESDL(77, accounts[4], 2, false, toEther(10)) + await bridge.transferRESDL(77, accounts[4], 2, false, toEther(10), 1) let lastRequestData = await onRamp.getLastRequestData() let lastRequestMsg = await onRamp.getLastRequestMessage() @@ -156,10 +156,13 @@ describe('RESDLTokenBridge', () => { [[sdlToken.address, 1000]] ) assert.equal(lastRequestMsg[3], linkToken.address) - assert.equal(lastRequestMsg[4], '0x11') + assert.equal( + lastRequestMsg[4], + '0x97a657c9' + ethers.utils.defaultAbiCoder.encode(['uint256'], [1]).slice(2) + ) await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') - await expect(bridge.transferRESDL(77, accounts[4], 1, false, toEther(1))).to.be.revertedWith( + await expect(bridge.transferRESDL(77, accounts[4], 1, false, toEther(1), 1)).to.be.revertedWith( 'FeeExceedsLimit()' ) @@ -172,7 +175,7 @@ describe('RESDLTokenBridge', () => { preFeeBalance = await linkToken.balanceOf(accounts[0]) - await bridge.transferRESDL(77, accounts[5], 3, false, toEther(10)) + await bridge.transferRESDL(77, accounts[5], 3, false, toEther(10), 1) lastRequestData = await onRamp.getLastRequestData() lastRequestMsg = await onRamp.getLastRequestMessage() @@ -204,7 +207,10 @@ describe('RESDLTokenBridge', () => { [[sdlToken.address, 500]] ) assert.equal(lastRequestMsg[3], linkToken.address) - assert.equal(lastRequestMsg[4], '0x11') + assert.equal( + lastRequestMsg[4], + '0x97a657c9' + ethers.utils.defaultAbiCoder.encode(['uint256'], [1]).slice(2) + ) await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') }) @@ -213,7 +219,7 @@ describe('RESDLTokenBridge', () => { let preFeeBalance = await ethers.provider.getBalance(accounts[0]) - await bridge.transferRESDL(77, accounts[4], 2, true, toEther(10), { value: toEther(10) }) + await bridge.transferRESDL(77, accounts[4], 2, true, toEther(10), 1, { value: toEther(10) }) let lastRequestData = await onRamp.getLastRequestData() let lastRequestMsg = await onRamp.getLastRequestMessage() @@ -247,26 +253,29 @@ describe('RESDLTokenBridge', () => { [[sdlToken.address, 1000]] ) assert.equal(lastRequestMsg[3], wrappedNative.address) - assert.equal(lastRequestMsg[4], '0x11') + assert.equal( + lastRequestMsg[4], + '0x97a657c9' + ethers.utils.defaultAbiCoder.encode(['uint256'], [1]).slice(2) + ) await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') }) it('transferRESDL validation should work correctly', async () => { await expect( - bridge.connect(signers[1]).transferRESDL(77, accounts[4], 1, false, toEther(10)) + bridge.connect(signers[1]).transferRESDL(77, accounts[4], 1, false, toEther(10), 1) ).to.be.revertedWith('SenderNotAuthorized()') await expect( - bridge.transferRESDL(77, ethers.constants.AddressZero, 1, false, toEther(10)) + bridge.transferRESDL(77, ethers.constants.AddressZero, 1, false, toEther(10), 1) ).to.be.revertedWith('InvalidReceiver()') - await expect(bridge.transferRESDL(78, accounts[4], 1, false, toEther(10))).to.be.revertedWith( - 'InvalidDestination()' - ) + await expect( + bridge.transferRESDL(78, accounts[4], 1, false, toEther(10), 1) + ).to.be.revertedWith('InvalidDestination()') - bridge.transferRESDL(77, accounts[4], 1, false, toEther(10)) + bridge.transferRESDL(77, accounts[4], 1, false, toEther(10), 1) }) it('ccipReceive should work correctly', async () => { - await bridge.transferRESDL(77, accounts[4], 2, true, toEther(10), { value: toEther(10) }) + await bridge.transferRESDL(77, accounts[4], 2, true, toEther(10), 1, { value: toEther(10) }) let success: any = await offRamp .connect(signers[1]) @@ -316,12 +325,4 @@ describe('RESDLTokenBridge', () => { ] ) }) - - it('should be able to set extra args', async () => { - await bridge.setExtraArgs(10, '0x22') - assert.equal(await bridge.extraArgsByChain(10), '0x22') - - await bridge.setExtraArgs(77, '0x33') - assert.equal(await bridge.extraArgsByChain(77), '0x33') - }) }) diff --git a/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts b/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts index f0bed948..007d1fdb 100644 --- a/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts +++ b/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts @@ -91,6 +91,7 @@ describe('SDLPoolCCIPControllerPrimary', () => { sdlToken.address, sdlPool.address, toEther(10), + accounts[0], ])) as SDLPoolCCIPControllerPrimary await linkToken.transfer(controller.address, toEther(100)) @@ -111,7 +112,7 @@ describe('SDLPoolCCIPControllerPrimary', () => { await sdlPool.setCCIPController(controller.address) await controller.setRESDLTokenBridge(accounts[5]) await controller.setRewardsInitiator(accounts[0]) - await controller.addWhitelistedChain(77, accounts[4], '0x11', '0x22') + await controller.addWhitelistedChain(77, accounts[4]) }) it('handleOutgoingRESDL should work correctly', async () => { @@ -167,7 +168,7 @@ describe('SDLPoolCCIPControllerPrimary', () => { }) it('adding/removing whitelisted chains should work correctly', async () => { - await controller.addWhitelistedChain(88, accounts[6], '0x33', '0x44') + await controller.addWhitelistedChain(88, accounts[6]) assert.deepEqual( (await controller.getWhitelistedChains()).map((d) => d.toNumber()), @@ -175,16 +176,12 @@ describe('SDLPoolCCIPControllerPrimary', () => { ) assert.equal(await controller.whitelistedDestinations(77), accounts[4]) assert.equal(await controller.whitelistedDestinations(88), accounts[6]) - assert.equal(await controller.updateExtraArgsByChain(77), '0x11') - assert.equal(await controller.rewardsExtraArgsByChain(77), '0x22') - assert.equal(await controller.updateExtraArgsByChain(88), '0x33') - assert.equal(await controller.rewardsExtraArgsByChain(88), '0x44') + await expect(controller.addWhitelistedChain(77, accounts[7])).to.be.revertedWith( + 'AlreadyAdded()' + ) await expect( - controller.addWhitelistedChain(77, accounts[7], '0x11', '0x22') - ).to.be.revertedWith('AlreadyAdded()') - await expect( - controller.addWhitelistedChain(99, ethers.constants.AddressZero, '0x11', '0x22') + controller.addWhitelistedChain(99, ethers.constants.AddressZero) ).to.be.revertedWith('InvalidDestination()') await controller.removeWhitelistedChain(77) @@ -193,8 +190,6 @@ describe('SDLPoolCCIPControllerPrimary', () => { [88] ) assert.equal(await controller.whitelistedDestinations(77), ethers.constants.AddressZero) - assert.equal(await controller.updateExtraArgsByChain(77), '0x') - assert.equal(await controller.rewardsExtraArgsByChain(77), '0x') await expect(controller.removeWhitelistedChain(77)).to.be.revertedWith('InvalidDestination()') }) @@ -205,7 +200,7 @@ describe('SDLPoolCCIPControllerPrimary', () => { await controller.approveRewardTokens([token1.address, token2.address]) await controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[0], 1) await token1.transferAndCall(rewardsPool1.address, toEther(50), '0x') - await controller.distributeRewards() + await controller.distributeRewards([1, 2]) let requestData = await onRamp.getLastRequestData() let requestMsg: any = await onRamp.getLastRequestMessage() @@ -214,7 +209,10 @@ describe('SDLPoolCCIPControllerPrimary', () => { assert.equal(requestData[1], controller.address) assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[4]) assert.equal(requestMsg[3], linkToken.address) - assert.equal(requestMsg[4], '0x22') + assert.equal( + requestMsg[4], + '0x97a657c9' + ethers.utils.defaultAbiCoder.encode(['uint256'], [1]).slice(2) + ) assert.deepEqual( requestMsg.tokenAmounts.map((d: any) => [d[0], fromEther(d[1])]), [[token1.address, 25]] @@ -237,7 +235,7 @@ describe('SDLPoolCCIPControllerPrimary', () => { let rewardsPool2 = await deploy('RewardsPool', [sdlPool.address, token2.address]) await sdlPool.addToken(token2.address, rewardsPool2.address) - await controller.addWhitelistedChain(88, accounts[7], '0x', '0x33') + await controller.addWhitelistedChain(88, accounts[7]) await sdlToken.transferAndCall( sdlPool.address, toEther(400), @@ -246,7 +244,7 @@ describe('SDLPoolCCIPControllerPrimary', () => { await controller.connect(signers[5]).handleOutgoingRESDL(88, accounts[0], 3) await token1.transferAndCall(rewardsPool1.address, toEther(200), '0x') await token2.transferAndCall(rewardsPool2.address, toEther(300), '0x') - await controller.distributeRewards() + await controller.distributeRewards([3, 4]) requestData = await onRamp.getLastRequestData() requestMsg = await onRamp.getLastRequestMessage() @@ -255,7 +253,10 @@ describe('SDLPoolCCIPControllerPrimary', () => { assert.equal(requestData[1], controller.address) assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[4]) assert.equal(requestMsg[3], linkToken.address) - assert.equal(requestMsg[4], '0x22') + assert.equal( + requestMsg[4], + '0x97a657c9' + ethers.utils.defaultAbiCoder.encode(['uint256'], [3]).slice(2) + ) assert.deepEqual( requestMsg.tokenAmounts.map((d: any) => [d[0], fromEther(d[1])]), [ @@ -272,7 +273,10 @@ describe('SDLPoolCCIPControllerPrimary', () => { assert.equal(requestData[1], controller.address) assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[7]) assert.equal(requestMsg[3], linkToken.address) - assert.equal(requestMsg[4], '0x33') + assert.equal( + requestMsg[4], + '0x97a657c9' + ethers.utils.defaultAbiCoder.encode(['uint256'], [4]).slice(2) + ) assert.deepEqual( requestMsg.tokenAmounts.map((d: any) => [d[0], fromEther(d[1])]), [ @@ -299,7 +303,7 @@ describe('SDLPoolCCIPControllerPrimary', () => { await offRamp.setTokenPool(wToken.address, wtokenPool.address) await controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[0], 1) await token1.transferAndCall(rewardsPool.address, toEther(500), '0x') - await controller.distributeRewards() + await controller.distributeRewards([1, 2]) let requestData = await onRamp.getLastRequestData() let requestMsg: any = await onRamp.getLastRequestMessage() @@ -329,16 +333,12 @@ describe('SDLPoolCCIPControllerPrimary', () => { assert.equal((await sdlPool.lastLockId()).toNumber(), 5) assert.equal(fromEther(await controller.reSDLSupplyByChain(77)), 1000) assert.equal(fromEther(await sdlPool.effectiveBalanceOf(controller.address)), 1000) - - let requestData = await onRamp.getLastRequestData() - let requestMsg: any = await onRamp.getLastRequestMessage() - assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 98) - assert.equal(fromEther(requestData[0]), 2) - assert.equal(requestData[1], controller.address) - assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[4]) - assert.equal(ethers.utils.defaultAbiCoder.decode(['uint256'], requestMsg[1])[0], 3) - assert.equal(requestMsg[3], linkToken.address) - assert.equal(requestMsg[4], '0x11') + assert.deepEqual( + await controller + .getQueuedUpdates() + .then((d) => d.map((v) => [v[0].toNumber(), v[1].toNumber()])), + [[77, 3]] + ) await offRamp .connect(signers[4]) @@ -353,18 +353,84 @@ describe('SDLPoolCCIPControllerPrimary', () => { assert.equal((await sdlPool.lastLockId()).toNumber(), 5) assert.equal(fromEther(await controller.reSDLSupplyByChain(77)), 900) assert.equal(fromEther(await sdlPool.effectiveBalanceOf(controller.address)), 900) + assert.deepEqual( + await controller + .getQueuedUpdates() + .then((d) => d.map((v) => [v[0].toNumber(), v[1].toNumber()])), + [ + [77, 3], + [77, 0], + ] + ) - requestData = await onRamp.getLastRequestData() - requestMsg = await onRamp.getLastRequestMessage() - assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 96) + await controller.addWhitelistedChain(88, accounts[6]) + let onRamp88 = (await deploy('CCIPOnRampMock', [[], [], linkToken.address])) as CCIPOnRampMock + let offRamp88 = (await deploy('CCIPOffRampMock', [router.address, [], []])) as CCIPOffRampMock + await router.applyRampUpdates([[88, onRamp88.address]], [], [[88, offRamp88.address]]) + await offRamp88 + .connect(signers[6]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 88, + ethers.utils.defaultAbiCoder.encode(['uint256', 'int256'], [2, toEther(200)]), + controller.address, + [] + ) + + assert.equal((await sdlPool.lastLockId()).toNumber(), 7) + assert.equal(fromEther(await controller.reSDLSupplyByChain(88)), 200) + assert.equal(fromEther(await sdlPool.effectiveBalanceOf(controller.address)), 1100) + assert.deepEqual( + await controller + .getQueuedUpdates() + .then((d) => d.map((v) => [v[0].toNumber(), v[1].toNumber()])), + [ + [77, 3], + [77, 0], + [88, 6], + ] + ) + }) + + it('executeQueuedUpdates should work correctly', async () => { + await offRamp + .connect(signers[4]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + ethers.utils.defaultAbiCoder.encode(['uint256', 'int256'], [3, toEther(1000)]), + controller.address, + [] + ) + + await controller.executeQueuedUpdates([1]) + + assert.deepEqual(await controller.getQueuedUpdates(), []) + + let requestData = await onRamp.getLastRequestData() + let requestMsg: any = await onRamp.getLastRequestMessage() + assert.equal(fromEther(await linkToken.balanceOf(controller.address)), 98) assert.equal(fromEther(requestData[0]), 2) assert.equal(requestData[1], controller.address) assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[4]) - assert.equal(ethers.utils.defaultAbiCoder.decode(['uint256'], requestMsg[1])[0], 0) + assert.equal(ethers.utils.defaultAbiCoder.decode(['uint256'], requestMsg[1])[0], 3) assert.equal(requestMsg[3], linkToken.address) - assert.equal(requestMsg[4], '0x11') + assert.equal( + requestMsg[4], + '0x97a657c9' + ethers.utils.defaultAbiCoder.encode(['uint256'], [1]).slice(2) + ) + + await offRamp + .connect(signers[4]) + .executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + ethers.utils.defaultAbiCoder.encode(['uint256', 'int256'], [0, toEther(-100)]), + controller.address, + [] + ) - await controller.addWhitelistedChain(88, accounts[6], '0x33', '0x') + await controller.addWhitelistedChain(88, accounts[6]) let onRamp88 = (await deploy('CCIPOnRampMock', [[], [], linkToken.address])) as CCIPOnRampMock let offRamp88 = (await deploy('CCIPOffRampMock', [router.address, [], []])) as CCIPOffRampMock await router.applyRampUpdates([[88, onRamp88.address]], [], [[88, offRamp88.address]]) @@ -378,9 +444,9 @@ describe('SDLPoolCCIPControllerPrimary', () => { [] ) - assert.equal((await sdlPool.lastLockId()).toNumber(), 7) - assert.equal(fromEther(await controller.reSDLSupplyByChain(88)), 200) - assert.equal(fromEther(await sdlPool.effectiveBalanceOf(controller.address)), 1100) + await controller.executeQueuedUpdates([1, 2]) + + assert.deepEqual(await controller.getQueuedUpdates(), []) requestData = await onRamp88.getLastRequestData() requestMsg = await onRamp88.getLastRequestMessage() @@ -390,7 +456,12 @@ describe('SDLPoolCCIPControllerPrimary', () => { assert.equal(ethers.utils.defaultAbiCoder.decode(['address'], requestMsg[0])[0], accounts[6]) assert.equal(ethers.utils.defaultAbiCoder.decode(['uint256'], requestMsg[1])[0].toNumber(), 6) assert.equal(requestMsg[3], linkToken.address) - assert.equal(requestMsg[4], '0x33') + assert.equal( + requestMsg[4], + '0x97a657c9' + ethers.utils.defaultAbiCoder.encode(['uint256'], [2]).slice(2) + ) + + await expect(controller.executeQueuedUpdates([1])).to.be.revertedWith('InvalidLength()') }) it('recoverTokens should work correctly', async () => { diff --git a/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts index 07412abf..79543dde 100644 --- a/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts +++ b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts @@ -9,8 +9,8 @@ import { SDLPoolCCIPControllerSecondary, SDLPoolSecondary, } from '../../../typechain-types' -import { time } from '@nomicfoundation/hardhat-network-helpers' import { Signer } from 'ethers' +import { time } from '@nomicfoundation/hardhat-network-helpers' const parseLock = (lock: any) => ({ amount: fromEther(lock[0]), @@ -93,7 +93,8 @@ describe('SDLPoolCCIPControllerSecondary', () => { 77, accounts[4], toEther(10), - '0x', + accounts[0], + 100, ])) as SDLPoolCCIPControllerSecondary await linkToken.transfer(controller.address, toEther(100)) @@ -155,25 +156,18 @@ describe('SDLPoolCCIPControllerSecondary', () => { }) }) - it('checkUpkeep should work correctly', async () => { - await token1.transfer(tokenPool.address, toEther(1000)) - let rewardsPool1 = await deploy('RewardsPool', [sdlPool.address, token1.address]) - await sdlPool.addToken(token1.address, rewardsPool1.address) - - assert.equal((await controller.checkUpkeep('0x'))[0], false) + it('shouldUpdate should work correctly', async () => { assert.equal(await controller.shouldUpdate(), false) - await offRamp - .connect(signers[4]) - .executeSingleMessage( - ethers.utils.formatBytes32String('messageId'), - 77, - '0x', - controller.address, - [{ token: token1.address, amount: toEther(25) }] - ) + await sdlToken.transferAndCall( + sdlPool.address, + toEther(100), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) + ) - assert.equal((await controller.checkUpkeep('0x'))[0], false) + assert.equal(await controller.shouldUpdate(), true) + + await controller.executeUpdate(1) assert.equal(await controller.shouldUpdate(), false) await sdlToken.transferAndCall( @@ -182,50 +176,23 @@ describe('SDLPoolCCIPControllerSecondary', () => { ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) ) - assert.equal((await controller.checkUpkeep('0x'))[0], false) assert.equal(await controller.shouldUpdate(), false) - await offRamp - .connect(signers[4]) - .executeSingleMessage( - ethers.utils.formatBytes32String('messageId'), - 77, - '0x', - controller.address, - [{ token: token1.address, amount: toEther(25) }] - ) - - assert.equal((await controller.checkUpkeep('0x'))[0], true) - assert.equal(await controller.shouldUpdate(), true) + await time.increase(100) - await controller.performUpkeep('0x') - assert.equal((await controller.checkUpkeep('0x'))[0], false) assert.equal(await controller.shouldUpdate(), false) }) - it('performUpkeep should work correctly', async () => { - await token1.transfer(tokenPool.address, toEther(1000)) - let rewardsPool1 = await deploy('RewardsPool', [sdlPool.address, token1.address]) - await sdlPool.addToken(token1.address, rewardsPool1.address) - - await expect(controller.performUpkeep('0x')).to.be.revertedWith('UpdateConditionsNotMet()') + it('executeUpdate should work correctly', async () => { + await expect(controller.executeUpdate(1)).to.be.revertedWith('UpdateConditionsNotMet()') await sdlToken.transferAndCall( sdlPool.address, toEther(100), ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 365 * 86400]) ) - await offRamp - .connect(signers[4]) - .executeSingleMessage( - ethers.utils.formatBytes32String('messageId'), - 77, - '0x', - controller.address, - [{ token: token1.address, amount: toEther(25) }] - ) - await controller.performUpkeep('0x') - await expect(controller.performUpkeep('0x')).to.be.revertedWith('UpdateConditionsNotMet()') + await controller.executeUpdate(1) + await expect(controller.executeUpdate(1)).to.be.revertedWith('UpdateConditionsNotMet()') let lastRequestData = await onRamp.getLastRequestData() let lastRequestMsg = await onRamp.getLastRequestMessage() @@ -246,6 +213,10 @@ describe('SDLPoolCCIPControllerSecondary', () => { [1, 200] ) assert.equal(lastRequestMsg[3], linkToken.address) + assert.equal( + lastRequestMsg[4], + '0x97a657c9' + ethers.utils.defaultAbiCoder.encode(['uint256'], [1]).slice(2) + ) await offRamp .connect(signers[4]) @@ -256,21 +227,13 @@ describe('SDLPoolCCIPControllerSecondary', () => { controller.address, [] ) - await expect(controller.performUpkeep('0x')).to.be.revertedWith('UpdateConditionsNotMet()') + await expect(controller.executeUpdate(1)).to.be.revertedWith('UpdateConditionsNotMet()') await sdlPool.connect(signers[1]).withdraw(2, toEther(10)) - await expect(controller.performUpkeep('0x')).to.be.revertedWith('UpdateConditionsNotMet()') + await expect(controller.executeUpdate(1)).to.be.revertedWith('UpdateConditionsNotMet()') - await offRamp - .connect(signers[4]) - .executeSingleMessage( - ethers.utils.formatBytes32String('messageId'), - 77, - '0x', - controller.address, - [{ token: token1.address, amount: toEther(25) }] - ) - await controller.performUpkeep('0x') + await time.increase(100) + await controller.executeUpdate(2) lastRequestData = await onRamp.getLastRequestData() lastRequestMsg = await onRamp.getLastRequestMessage() @@ -291,6 +254,10 @@ describe('SDLPoolCCIPControllerSecondary', () => { [0, -10] ) assert.equal(lastRequestMsg[3], linkToken.address) + assert.equal( + lastRequestMsg[4], + '0x97a657c9' + ethers.utils.defaultAbiCoder.encode(['uint256'], [2]).slice(2) + ) }) it('ccipReceive should work correctly for reward distributions', async () => { @@ -374,25 +341,12 @@ describe('SDLPoolCCIPControllerSecondary', () => { }) it('ccipReceive should work correctly for incoming updates', async () => { - await token1.transfer(tokenPool.address, toEther(1000)) - let rewardsPool1 = await deploy('RewardsPool', [sdlPool.address, token1.address]) - await sdlPool.addToken(token1.address, rewardsPool1.address) - await sdlToken.transferAndCall( sdlPool.address, toEther(300), ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) ) - await offRamp - .connect(signers[4]) - .executeSingleMessage( - ethers.utils.formatBytes32String('messageId'), - 77, - '0x', - controller.address, - [{ token: token1.address, amount: toEther(30) }] - ) - await controller.performUpkeep('0x') + await controller.executeUpdate(1) let success: any = await offRamp .connect(signers[5]) diff --git a/test/core/rewards-initiator.test.ts b/test/core/rewards-initiator.test.ts index d4bbeefb..5ea20783 100644 --- a/test/core/rewards-initiator.test.ts +++ b/test/core/rewards-initiator.test.ts @@ -163,7 +163,7 @@ describe('RewardsInitiator', () => { await strategy1.simulateSlash(toEther(10)) await strategy3.simulateSlash(toEther(10)) - await rewardsInitiator.updateRewards([0, 2], '0x') + await rewardsInitiator.updateRewards([0, 2], '0x', []) assert.equal(fromEther(await strategy1.getDepositChange()), 0) assert.equal(fromEther(await strategy2.getDepositChange()), 100) @@ -173,7 +173,7 @@ describe('RewardsInitiator', () => { await token.transfer(strategy2.address, toEther(10)) await token.transfer(strategy3.address, toEther(20)) - await rewardsInitiator.updateRewards([0, 1, 2], '0x') + await rewardsInitiator.updateRewards([0, 1, 2], '0x', []) assert.equal(fromEther(await strategy1.getDepositChange()), 0) assert.equal(fromEther(await strategy2.getDepositChange()), 0) From 9e7cb18ec432bd9c7eb3f4feae4b2a761df73251 Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 4 Mar 2024 08:56:01 +1300 Subject: [PATCH 41/42] ccip stage 2 deployment scripts --- hardhat.config.ts | 5 +- .../{ => stage-1}/deploy-ccip-dest-tokens.ts | 2 +- .../ccip/{ => stage-1}/deploy-ccip-stage-1.ts | 4 +- .../ccip/stage-2/1-deploy-primary-chain.ts | 103 ++++++++++++++++ .../ccip/stage-2/2-deploy-secondary-chain.ts | 114 ++++++++++++++++++ .../ccip/stage-2/3-upgrade-primary-chain.ts | 86 +++++++++++++ scripts/utils/helpers.ts | 4 +- 7 files changed, 311 insertions(+), 7 deletions(-) rename scripts/prod/ccip/{ => stage-1}/deploy-ccip-dest-tokens.ts (92%) rename scripts/prod/ccip/{ => stage-1}/deploy-ccip-stage-1.ts (87%) create mode 100644 scripts/prod/ccip/stage-2/1-deploy-primary-chain.ts create mode 100644 scripts/prod/ccip/stage-2/2-deploy-secondary-chain.ts create mode 100644 scripts/prod/ccip/stage-2/3-upgrade-primary-chain.ts diff --git a/hardhat.config.ts b/hardhat.config.ts index 44fd5fe2..10a9efd4 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -31,12 +31,13 @@ const config: HardhatUserConfig = { url: 'http://127.0.0.1:8545', accounts, }, - rinkeby: { + sepolia: { url: '', accounts, }, - ropsten: { + 'arbitrum-sepolia': { url: '', + chainId: 421614, accounts, }, mainnet: { diff --git a/scripts/prod/ccip/deploy-ccip-dest-tokens.ts b/scripts/prod/ccip/stage-1/deploy-ccip-dest-tokens.ts similarity index 92% rename from scripts/prod/ccip/deploy-ccip-dest-tokens.ts rename to scripts/prod/ccip/stage-1/deploy-ccip-dest-tokens.ts index d1e0faff..7c2b443c 100644 --- a/scripts/prod/ccip/deploy-ccip-dest-tokens.ts +++ b/scripts/prod/ccip/stage-1/deploy-ccip-dest-tokens.ts @@ -1,4 +1,4 @@ -import { updateDeployments, deploy } from '../../utils/deployment' +import { updateDeployments, deploy } from '../../../utils/deployment' // SDL const sdl = { diff --git a/scripts/prod/ccip/deploy-ccip-stage-1.ts b/scripts/prod/ccip/stage-1/deploy-ccip-stage-1.ts similarity index 87% rename from scripts/prod/ccip/deploy-ccip-stage-1.ts rename to scripts/prod/ccip/stage-1/deploy-ccip-stage-1.ts index e53582f0..11b024e1 100644 --- a/scripts/prod/ccip/deploy-ccip-stage-1.ts +++ b/scripts/prod/ccip/stage-1/deploy-ccip-stage-1.ts @@ -1,5 +1,5 @@ -import { WrappedTokenBridge } from '../../../typechain-types' -import { updateDeployments, deploy, getContract } from '../../utils/deployment' +import { WrappedTokenBridge } from '../../../../typechain-types' +import { updateDeployments, deploy, getContract } from '../../../utils/deployment' // should be deployed on primary chain (Ethereum Mainnet) diff --git a/scripts/prod/ccip/stage-2/1-deploy-primary-chain.ts b/scripts/prod/ccip/stage-2/1-deploy-primary-chain.ts new file mode 100644 index 00000000..4881fdd0 --- /dev/null +++ b/scripts/prod/ccip/stage-2/1-deploy-primary-chain.ts @@ -0,0 +1,103 @@ +import { toEther } from '../../../utils/helpers' +import { updateDeployments, deploy } from '../../../utils/deployment' +import { RewardsInitiator, SDLPoolCCIPControllerPrimary } from '../../../../typechain-types' +import { ethers, upgrades } from 'hardhat' +import { getContract } from '../../../utils/deployment' + +// Execute on Ethereum Mainnet + +const ccipRouter = '0x80226fc0Ee2b096224EeAc085Bb9a8cba1146f7D' +const multisigAddress = '0xB351EC0FEaF4B99FdFD36b484d9EC90D0422493D' + +// Linear Boost Controller +const LinearBoostControllerParams = { + minLockingDuration: 86400, // minimum locking duration in seconds + maxLockingDuration: 4 * 365 * 86400, // maximum locking duration in seconds + maxBoost: 8, // maximum boost amount +} +// SDL Pool CCIP Controller Primary +const SDLPoolCCIPControllerParams = { + maxLINKFee: toEther(10), // max LINK fee to paid on outgoing CCIP updates + updateInitiator: '', // address authorized to send CCIP updates +} +// Rewards Initiator +const RewardsInitiatorParams = { + whitelistedCaller: '', // address authorized to initiate rebase and rewards distribution +} + +async function main() { + const sdlPool = await getContract('SDLPool') + const sdlToken = await getContract('SDLToken') + const linkToken = await getContract('LINKToken') + const wstLINKToken = await getContract('LINK_WrappedSDToken') + const stakingPool = await getContract('LINK_StakingPool') + + const boostController = await deploy('LinearBoostController', [ + LinearBoostControllerParams.minLockingDuration, + LinearBoostControllerParams.maxLockingDuration, + LinearBoostControllerParams.maxBoost, + ]) + console.log('LinearBoostController deployed: ', boostController.address) + + const sdlPoolPrimaryImp = (await upgrades.prepareUpgrade( + sdlPool.address, + await ethers.getContractFactory('SDLPoolPrimary'), + { + kind: 'uups', + } + )) as string + console.log('SDLPoolPrimary implementation deployed at: ', sdlPoolPrimaryImp) + + const ccipController = (await deploy('SDLPoolCCIPControllerPrimary', [ + ccipRouter, + linkToken.address, + sdlToken.address, + sdlPool.address, + SDLPoolCCIPControllerParams.maxLINKFee, + SDLPoolCCIPControllerParams.updateInitiator, + ])) as SDLPoolCCIPControllerPrimary + console.log('SDLPoolCCIPControllerPrimary deployed: ', ccipController.address) + + const reSDLTokenBridge = await deploy('RESDLTokenBridge', [ + linkToken.address, + sdlToken.address, + sdlPool.address, + ccipController.address, + ]) + console.log('RESDLTokenBridge deployed: ', reSDLTokenBridge.address) + + const rewardsInitiator = (await deploy('RewardsInitiator', [ + stakingPool.address, + ccipController.address, + ])) as RewardsInitiator + console.log('RewardsInitiator deployed: ', rewardsInitiator.address) + + updateDeployments({ + LinearBoostController: boostController.address, + SDLPoolCCIPControllerPrimary: ccipController.address, + RESDLTokenBridge: reSDLTokenBridge.address, + RewardsInitiator: rewardsInitiator.address, + }) + + await (await boostController.transferOwnership(multisigAddress)).wait() + + await ( + await rewardsInitiator.whitelistCaller(RewardsInitiatorParams.whitelistedCaller, true) + ).wait() + await (await rewardsInitiator.transferOwnership(multisigAddress)).wait() + + await ( + await ccipController.setWrappedRewardToken(stakingPool.address, wstLINKToken.address) + ).wait() + await (await ccipController.approveRewardTokens([wstLINKToken.address])).wait() + await (await ccipController.setRESDLTokenBridge(reSDLTokenBridge.address)).wait() + await (await ccipController.setRewardsInitiator(rewardsInitiator.address)).wait() + await (await ccipController.transferOwnership(multisigAddress)).wait() +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/prod/ccip/stage-2/2-deploy-secondary-chain.ts b/scripts/prod/ccip/stage-2/2-deploy-secondary-chain.ts new file mode 100644 index 00000000..bb7ccc18 --- /dev/null +++ b/scripts/prod/ccip/stage-2/2-deploy-secondary-chain.ts @@ -0,0 +1,114 @@ +import { toEther } from '../../../utils/helpers' +import { + updateDeployments, + deploy, + deployUpgradeable, + getContract, +} from '../../../utils/deployment' +import { + RESDLTokenBridge, + SDLPoolCCIPControllerSecondary, + SDLPoolSecondary, +} from '../../../../typechain-types' + +// Execute on Arbitrum Mainnet + +const ccipRouter = '0x141fa059441E0ca23ce184B6A78bafD2A517DdE8' +const multisigAddress = '' + +const primaryChainSelector = '5009297550715157269' // ETH Mainnet +const primaryChainSDLPoolCCIPController = '' // ETH Mainnet + +// Linear Boost Controller +const LinearBoostControllerParams = { + minLockingDuration: 86400, // minimum locking duration in seconds + maxLockingDuration: 4 * 365 * 86400, // maximum locking duration + maxBoost: 8, // maximum boost amount +} +// SDL Pool Secondary +const SDLPoolParams = { + derivativeTokenName: 'Reward Escrowed SDL', // SDL staking derivative token name + derivativeTokenSymbol: 'reSDL', // SDL staking derivative token symbol + queuedNewLockLimit: 50, // max number of queued new locks a user can have at once + baseURI: '', // base URI for reSDL NFTs +} +// SDL Pool CCIP Controller Secondary +const SDLPoolCCIPControllerParams = { + maxLINKFee: toEther(10), // max LINK fee to be paid on outgoing CCIP updates + updateInitiator: '', // address authorized to send CCIP updates + minTimeBetweenUpdates: '82800', // min time between updates in seconds +} + +async function main() { + const sdlToken = await getContract('SDLToken') + const linkToken = await getContract('LINKToken') + const wstLINKToken = await getContract('LINK_WrappedSDToken') + + const boostController = await deploy('LinearBoostController', [ + LinearBoostControllerParams.minLockingDuration, + LinearBoostControllerParams.maxLockingDuration, + LinearBoostControllerParams.maxBoost, + ]) + console.log('LinearBoostController deployed: ', boostController.address) + + const sdlPool = (await deployUpgradeable('SDLPoolSecondary', [ + SDLPoolParams.derivativeTokenName, + SDLPoolParams.derivativeTokenSymbol, + sdlToken.address, + boostController.address, + SDLPoolParams.queuedNewLockLimit, + ])) as SDLPoolSecondary + console.log('SDLPoolSecondary deployed: ', sdlPool.address) + + const rewardsPool = await deploy('RewardsPool', [sdlPool.address, wstLINKToken.address]) + console.log('wstLINK_SDLRewardsPool deployed: ', rewardsPool.address) + + const ccipController = (await deploy('SDLPoolCCIPControllerSecondary', [ + ccipRouter, + linkToken.address, + sdlToken.address, + sdlPool.address, + primaryChainSelector, + primaryChainSDLPoolCCIPController, + SDLPoolCCIPControllerParams.maxLINKFee, + SDLPoolCCIPControllerParams.updateInitiator, + SDLPoolCCIPControllerParams.minTimeBetweenUpdates, + ])) as SDLPoolCCIPControllerSecondary + console.log('SDLPoolCCIPControllerSecondary deployed: ', ccipController.address) + + const reSDLTokenBridge = (await deploy('RESDLTokenBridge', [ + linkToken.address, + sdlToken.address, + sdlPool.address, + ccipController.address, + ])) as RESDLTokenBridge + console.log('RESDLTokenBridge deployed: ', reSDLTokenBridge.address) + + await (await boostController.transferOwnership(multisigAddress)).wait() + + await (await sdlPool.setCCIPController(ccipController.address)).wait() + await (await sdlPool.addToken(wstLINKToken.address, rewardsPool.address)).wait() + await (await sdlPool.setBaseURI(SDLPoolParams.baseURI)).wait() + await (await sdlPool.transferOwnership(multisigAddress)).wait() + + await (await ccipController.setRESDLTokenBridge(reSDLTokenBridge.address)).wait() + await (await ccipController.transferOwnership(multisigAddress)).wait() + + updateDeployments( + { + SDLPoolSecondary: sdlPool.address, + LinearBoostController: boostController.address, + wstLINK_SDLRewardsPool: rewardsPool.address, + SDLPoolCCIPControllerSecondary: ccipController.address, + RESDLTokenBridge: reSDLTokenBridge.address, + }, + { wstLINK_SDLRewardsPool: 'RewardsPool' } + ) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/prod/ccip/stage-2/3-upgrade-primary-chain.ts b/scripts/prod/ccip/stage-2/3-upgrade-primary-chain.ts new file mode 100644 index 00000000..3e3a0e4d --- /dev/null +++ b/scripts/prod/ccip/stage-2/3-upgrade-primary-chain.ts @@ -0,0 +1,86 @@ +import { getContract } from '../../../utils/deployment' +import { SDLPoolCCIPControllerPrimary, SDLPoolPrimary } from '../../../../typechain-types' +import { getAccounts } from '../../../utils/helpers' +import Safe, { EthersAdapter } from '@safe-global/protocol-kit' +import SafeApiKit from '@safe-global/api-kit' +import { ethers } from 'hardhat' +import { MetaTransactionData } from '@safe-global/safe-core-sdk-types' + +// Execute on Ethereum Mainnet + +const multisigAddress = '0xB351EC0FEaF4B99FdFD36b484d9EC90D0422493D' +const secondaryChainSelector = '4949039107694359620' // Arbitrum Mainnet +const secondaryChainSDLPoolCCIPController = '' // Arbitrum Mainnet +const sdlPoolPrimaryImplementation = '' + +async function main() { + const { signers, accounts } = await getAccounts() + const ethAdapter = new EthersAdapter({ + ethers, + signerOrProvider: signers[0], + }) + const safeSdk = await Safe.create({ ethAdapter, safeAddress: multisigAddress }) + const safeService = new SafeApiKit({ + txServiceUrl: 'https://safe-transaction-mainnet.safe.global', + ethAdapter, + }) + + const sdlPool = (await getContract('SDLPool')) as SDLPoolPrimary + const ccipController = (await getContract( + 'SDLPoolCCIPControllerPrimary' + )) as SDLPoolCCIPControllerPrimary + + const safeTransactionData: MetaTransactionData[] = [ + { + to: sdlPool.address, + data: + ( + await sdlPool.populateTransaction.upgradeToAndCall( + sdlPoolPrimaryImplementation, + sdlPool.interface.encodeFunctionData('initialize', [ + '', + '', + ethers.constants.AddressZero, + ethers.constants.AddressZero, + ]) + ) + ).data || '', + value: '0', + }, + { + to: sdlPool.address, + data: + (await sdlPool.populateTransaction.setCCIPController(ccipController.address)).data || '', + value: '0', + }, + { + to: ccipController.address, + data: + ( + await ccipController.populateTransaction.addWhitelistedChain( + secondaryChainSelector, + secondaryChainSDLPoolCCIPController + ) + ).data || '', + value: '0', + }, + ] + const safeTransaction = await safeSdk.createTransaction({ safeTransactionData }) + const safeTxHash = await safeSdk.getTransactionHash(safeTransaction) + const senderSignature = await safeSdk.signTransactionHash(safeTxHash) + + await safeService.proposeTransaction({ + safeAddress: multisigAddress, + safeTransactionData: safeTransaction.data, + safeTxHash, + senderAddress: accounts[0], + senderSignature: senderSignature.data, + }) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) diff --git a/scripts/utils/helpers.ts b/scripts/utils/helpers.ts index 3827998b..13580793 100644 --- a/scripts/utils/helpers.ts +++ b/scripts/utils/helpers.ts @@ -1,5 +1,5 @@ import { ethers } from 'hardhat' -import { BigNumber } from 'ethers' +import { BigNumber, Signer } from 'ethers' import { ERC677 } from '../../typechain-types' export const toEther = (amount: string | number) => { @@ -10,7 +10,7 @@ export const fromEther = (amount: BigNumber) => { return Number(ethers.utils.formatEther(amount)) } -export const getAccounts = async () => { +export const getAccounts = async (): Promise => { const signers = await ethers.getSigners() const accounts = await Promise.all(signers.map(async (signer) => signer.getAddress())) return { signers, accounts } From bb51455ed8f48f0e286ab5212bdf460ffd5d8a0f Mon Sep 17 00:00:00 2001 From: BkChoy Date: Mon, 4 Mar 2024 15:39:51 +1300 Subject: [PATCH 42/42] removed ownable from RESDLTokenBridge --- contracts/core/ccip/RESDLTokenBridge.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/core/ccip/RESDLTokenBridge.sol b/contracts/core/ccip/RESDLTokenBridge.sol index 098d46b3..0eac974f 100644 --- a/contracts/core/ccip/RESDLTokenBridge.sol +++ b/contracts/core/ccip/RESDLTokenBridge.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.15; import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol"; import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "../interfaces/ISDLPool.sol"; @@ -13,7 +12,7 @@ import "../interfaces/ISDLPoolCCIPController.sol"; * @title reSDL Token Bridge * @notice Handles CCIP transfers of reSDL NFTs */ -contract RESDLTokenBridge is Ownable { +contract RESDLTokenBridge { using SafeERC20 for IERC20; IERC20 public linkToken;