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/contracts/core/RewardsInitiator.sol b/contracts/core/RewardsInitiator.sol new file mode 100644 index 00000000..d69f3d57 --- /dev/null +++ b/contracts/core/RewardsInitiator.sol @@ -0,0 +1,110 @@ +// 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"; + +/** + * @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 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); + sdlPoolCCIPController = ISDLPoolCCIPControllerPrimary(_sdlPoolCCIPController); + } + + /** + * @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, + uint256[] calldata _gasLimits + ) external { + if (!whitelistedCallers[msg.sender]) revert SenderNotAuthorized(); + stakingPool.updateStrategyRewards(_strategyIdxs, _data); + sdlPoolCCIPController.distributeRewards(_gasLimits); + } + + /** + * @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 + **/ + function checkUpkeep(bytes calldata) external view returns (bool, bytes memory) { + address[] memory strategies = stakingPool.getStrategies(); + bool[] memory strategiesToUpdate = new bool[](strategies.length); + uint256 totalStrategiesToUpdate; + + for (uint256 i = 0; i < strategies.length; ++i) { + IStrategy strategy = IStrategy(strategies[i]); + if (strategy.getDepositChange() < 0) { + strategiesToUpdate[i] = true; + totalStrategiesToUpdate++; + } + } + + if (totalStrategiesToUpdate != 0) { + uint256[] memory strategyIdxs = new uint256[](totalStrategiesToUpdate); + uint256 strategiesAdded; + + for (uint256 i = 0; i < strategiesToUpdate.length; ++i) { + if (strategiesToUpdate[i]) { + strategyIdxs[strategiesAdded] = i; + strategiesAdded++; + } + } + + return (true, abi.encode(strategyIdxs)); + } + + return (false, "0x"); + } + + /** + * @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[])); + + 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, ""); + } + + /** + * @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/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/SlashingKeeper.sol b/contracts/core/SlashingKeeper.sol deleted file mode 100644 index 1cf6bfe5..00000000 --- a/contracts/core/SlashingKeeper.sol +++ /dev/null @@ -1,69 +0,0 @@ -// 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"; - -/** - * @title Slashing Keeper - * @notice Updates strategy rewards if any losses have been incurred - */ -contract SlashingKeeper is KeeperCompatibleInterface { - IStakingPool public stakingPool; - - constructor(address _stakingPool) { - stakingPool = IStakingPool(_stakingPool); - } - - /** - * @notice returns whether or not rewards should be updated and the strategies to update - * @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) { - address[] memory strategies = stakingPool.getStrategies(); - bool[] memory strategiesToUpdate = new bool[](strategies.length); - uint256 totalStrategiesToUpdate; - - for (uint256 i = 0; i < strategies.length; i++) { - IStrategy strategy = IStrategy(strategies[i]); - if (strategy.getDepositChange() < 0) { - strategiesToUpdate[i] = true; - totalStrategiesToUpdate++; - } - } - - if (totalStrategiesToUpdate > 0) { - uint256[] memory strategyIdxs = new uint256[](totalStrategiesToUpdate); - uint256 strategiesAdded; - - for (uint256 i = 0; i < strategiesToUpdate.length; i++) { - if (strategiesToUpdate[i]) { - strategyIdxs[strategiesAdded] = i; - strategiesAdded++; - } - } - - return (true, abi.encode(strategyIdxs)); - } - - return (false, "0x"); - } - - /** - * @notice Updates rewards - * @param _performData abi encoded list of strategy indexes to update - */ - function performUpkeep(bytes calldata _performData) external override { - 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"); - } - 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/base/RewardsPoolController.sol b/contracts/core/base/RewardsPoolController.sol index 25ab6d53..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(); @@ -148,8 +151,8 @@ 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 { - require(!isTokenSupported(_token), "Token is already supported"); + function addToken(address _token, address _rewardsPool) public virtual onlyOwner { + 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/RESDLTokenBridge.sol b/contracts/core/ccip/RESDLTokenBridge.sol new file mode 100644 index 00000000..0eac974f --- /dev/null +++ b/contracts/core/ccip/RESDLTokenBridge.sol @@ -0,0 +1,229 @@ +// 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 "@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 { + using SafeERC20 for IERC20; + + IERC20 public linkToken; + + IERC20 public sdlToken; + ISDLPool public sdlPool; + ISDLPoolCCIPController public sdlPoolCCIPController; + + 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 sourceChainSelector, + address indexed sender, + address receiver, + uint256 tokenId + ); + + error InsufficientFee(); + error TransferFailed(); + error FeeExceedsLimit(); + error SenderNotAuthorized(); + error InvalidReceiver(); + error InvalidMsgValue(); + + /** + * @notice Initializes the contract + * @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 _linkToken, + address _sdlToken, + address _sdlPool, + address _sdlPoolCCIPController + ) { + linkToken = IERC20(_linkToken); + sdlToken = IERC20(_sdlToken); + sdlPool = ISDLPool(_sdlPool); + sdlPoolCCIPController = ISDLPoolCCIPController(_sdlPoolCCIPController); + } + + modifier onlySDLPoolCCIPController() { + if (msg.sender != address(sdlPoolCCIPController)) revert SenderNotAuthorized(); + _; + } + + /** + * @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 _gasLimit gas limit to use for CCIP message on destination chain + **/ + function transferRESDL( + uint64 _destinationChainSelector, + address _receiver, + uint256 _tokenId, + bool _payNative, + uint256 _maxLINKFee, + uint256 _gasLimit + ) external payable returns (bytes32 messageId) { + if (msg.sender != sdlPool.ownerOf(_tokenId)) revert SenderNotAuthorized(); + if (_receiver == address(0)) revert InvalidReceiver(); + if (_payNative == false && msg.value != 0) revert InvalidMsgValue(); + + (address destination, ISDLPool.RESDLToken memory reSDLToken) = sdlPoolCCIPController.handleOutgoingRESDL( + _destinationChainSelector, + msg.sender, + _tokenId + ); + + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + _receiver, + _tokenId, + reSDLToken, + destination, + _payNative ? address(0) : address(linkToken), + _gasLimit + ); + + uint256 fees = IRouterClient(sdlPoolCCIPController.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); + + if (_payNative) { + if (fees > msg.value) revert InsufficientFee(); + messageId = sdlPoolCCIPController.ccipSend{value: fees}(_destinationChainSelector, evm2AnyMessage); + if (fees < msg.value) { + (bool success, ) = msg.sender.call{value: msg.value - fees}(""); + if (!success) revert TransferFailed(); + } + } else { + if (fees > _maxLINKFee) revert FeeExceedsLimit(); + linkToken.safeTransferFrom(msg.sender, address(sdlPoolCCIPController), fees); + messageId = sdlPoolCCIPController.ccipSend(_destinationChainSelector, evm2AnyMessage); + } + + emit TokenTransferred( + messageId, + _destinationChainSelector, + msg.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 _gasLimit gas limit to use for CCIP message on destination chain + * @return fee current fee + **/ + 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), + _gasLimit + ); + + return IRouterClient(sdlPoolCCIPController.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); + } + + /** + * @notice Processes a received message + * @dev handles incoming reSDL transfers + * @param _message CCIP message + **/ + function ccipReceive(Client.Any2EVMMessage memory _message) external onlySDLPoolCCIPController { + address sender = abi.decode(_message.sender, (address)); + + ( + 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); + } + + /** + * @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 _gasLimit gas limit to use for CCIP message on destination chain + **/ + function _buildCCIPMessage( + address _receiver, + uint256 _tokenId, + ISDLPool.RESDLToken memory _reSDLToken, + address _destination, + address _feeTokenAddress, + uint256 _gasLimit + ) 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: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: _gasLimit})), + feeToken: _feeTokenAddress + }); + + return evm2AnyMessage; + } +} diff --git a/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol new file mode 100644 index 00000000..ce408190 --- /dev/null +++ b/contracts/core/ccip/SDLPoolCCIPControllerPrimary.sol @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import "./base/SDLPoolCCIPController.sol"; +import "../interfaces/IERC677.sol"; + +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; + + struct QueuedUpdate { + uint64 chainSelector; + uint192 mintStartIndex; + } + + uint64[] internal whitelistedChains; + mapping(uint64 => address) public whitelistedDestinations; + 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); + event ChainRemoved(uint64 indexed chainSelector, address destination); + event SetWrappedRewardToken(address indexed token, address rewardToken); + + error InvalidLength(); + + /** + * @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 + * @param _updateInitiator address of the update initiator + **/ + constructor( + address _router, + address _linkToken, + address _sdlToken, + address _sdlPool, + 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(uint256[] calldata _gasLimits) external onlyRewardsInitiator { + 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, ""); + tokens[i] = 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], 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; + } + + /** + * @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 override onlyBridge returns (address, ISDLPool.RESDLToken memory) { + if (whitelistedDestinations[_destinationChainSelector] == address(0)) revert InvalidDestination(); + ISDLPool.RESDLToken memory reSDLToken = ISDLPoolPrimary(sdlPool).handleOutgoingRESDL( + _sender, + _tokenId, + address(this) + ); + reSDLSupplyByChain[_destinationChainSelector] += reSDLToken.amount + reSDLToken.boostAmount; + return (whitelistedDestinations[_destinationChainSelector], reSDLToken); + } + + /** + * @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 override onlyBridge { + sdlToken.safeTransfer(sdlPool, _reSDLToken.amount); + ISDLPoolPrimary(sdlPool).handleIncomingRESDL(_receiver, _tokenId, _reSDLToken); + reSDLSupplyByChain[_sourceChainSelector] -= _reSDLToken.amount + _reSDLToken.boostAmount; + } + + /** + * @notice Returns a list of all whitelisted chains + * @return list of whitelisted chain ids + **/ + function getWhitelistedChains() external view returns (uint64[] memory) { + 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 + **/ + 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; + emit ChainAdded(_chainSelector, _destination); + } + + /** + * @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]; + } + + /** + * @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 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 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 _gasLimit + ) 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, + _gasLimit + ); + + 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); + } + + /** + * @notice Processes a received message + * @dev handles incoming updates from a secondary chain and sends an update in response + * @param _message CCIP message + **/ + function _ccipReceive(Client.Any2EVMMessage memory _message) internal override { + uint64 sourceChainSelector = _message.sourceChainSelector; + + (uint256 numNewRESDLTokens, int256 totalRESDLSupplyChange) = abi.decode(_message.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); + + queuedUpdates.push(QueuedUpdate(sourceChainSelector, uint192(mintStartIndex))); + + emit MessageReceived(_message.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 + * @param _gasLimit gas limit to use for CCIP message on destination chain + **/ + function _ccipSendUpdate( + uint64 _destinationChainSelector, + uint256 _mintStartIndex, + uint256 _gasLimit + ) internal { + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + whitelistedDestinations[_destinationChainSelector], + _mintStartIndex, + new address[](0), + new uint256[](0), + _gasLimit + ); + + 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); + } + + /** + * @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 _gasLimit gas limit to use for CCIP message on destination chain + **/ + function _buildCCIPMessage( + address _destination, + uint256 _mintStartIndex, + address[] memory _tokens, + uint256[] memory _tokenAmounts, + uint256 _gasLimit + ) 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: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: _gasLimit})), + feeToken: address(linkToken) + }); + + 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 new file mode 100644 index 00000000..04bb6f11 --- /dev/null +++ b/contracts/core/ccip/SDLPoolCCIPControllerSecondary.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import "./base/SDLPoolCCIPController.sol"; + +interface ISDLPoolSecondary is ISDLPool { + function handleOutgoingUpdate() external returns (uint256, int256); + + function handleIncomingUpdate(uint256 _mintStartIndex) external; + + 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; + + address public updateInitiator; + + uint64 timeOfLastUpdate; + uint64 minTimeBetweenUpdates; + + uint64 public immutable primaryChainSelector; + address public immutable primaryChainDestination; + + 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 _updateInitiator address of the update initiator + * @param _minTimeBetweenUpdates min time between updates + **/ + constructor( + address _router, + address _linkToken, + address _sdlToken, + address _sdlPool, + uint64 _primaryChainSelector, + address _primaryChainDestination, + uint256 _maxLINKFee, + address _updateInitiator, + uint64 _minTimeBetweenUpdates + ) SDLPoolCCIPController(_router, _linkToken, _sdlToken, _sdlPool, _maxLINKFee) { + primaryChainSelector = _primaryChainSelector; + primaryChainDestination = _primaryChainDestination; + updateInitiator = _updateInitiator; + minTimeBetweenUpdates = _minTimeBetweenUpdates; + } + + modifier onlyUpdateInitiator() { + if (msg.sender != updateInitiator) revert SenderNotAuthorized(); + _; + } + + /** + * @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 executeUpdate(uint256 _gasLimit) external onlyUpdateInitiator { + if (!shouldUpdate()) revert UpdateConditionsNotMet(); + + timeOfLastUpdate = uint64(block.timestamp); + _initiateUpdate(primaryChainSelector, primaryChainDestination, _gasLimit); + } + + /** + * @notice Returns whether an update should be sent to the primary chain + * @return whether update should be sent + **/ + function shouldUpdate() public view returns (bool) { + return ISDLPoolSecondary(sdlPool).shouldUpdate() && block.timestamp > timeOfLastUpdate + minTimeBetweenUpdates; + } + + /** + * @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 _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))); + } + + /** + * @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 _reSDLToken reSDL token + **/ + function handleIncomingRESDL( + uint64, + address _receiver, + uint256 _tokenId, + ISDLPool.RESDLToken calldata _reSDLToken + ) external override onlyBridge { + sdlToken.safeTransfer(sdlPool, _reSDLToken.amount); + ISDLPoolSecondary(sdlPool).handleIncomingRESDL(_receiver, _tokenId, _reSDLToken); + } + + /** + * @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 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 _gasLimit gas limit to use for CCIP message on destination chain + **/ + function _initiateUpdate( + uint64 _destinationChainSelector, + address _destination, + uint256 _gasLimit + ) internal { + (uint256 numNewRESDLTokens, int256 totalRESDLSupplyChange) = ISDLPoolSecondary(sdlPool).handleOutgoingUpdate(); + + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + _destination, + numNewRESDLTokens, + totalRESDLSupplyChange, + _gasLimit + ); + + 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); + } + + /** + * @notice Processes a received message + * @dev handles incoming updates and reward distributions from the primary chain + * @param _message CCIP message + **/ + function _ccipReceive(Client.Any2EVMMessage memory _message) internal override { + 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] = _message.destTokenAmounts[i].token; + IERC20(rewardTokens[i]).safeTransfer(sdlPool, _message.destTokenAmounts[i].amount); + } + ISDLPoolSecondary(sdlPool).distributeTokens(rewardTokens); + } + } else { + uint256 mintStartIndex = abi.decode(_message.data, (uint256)); + ISDLPoolSecondary(sdlPool).handleIncomingUpdate(mintStartIndex); + } + + emit MessageReceived(_message.messageId, _message.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 _gasLimit gas limit to use for CCIP message on destination chain + **/ + function _buildCCIPMessage( + address _destination, + uint256 _numNewRESDLTokens, + int256 _totalRESDLSupplyChange, + 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: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: _gasLimit})), + feeToken: address(linkToken) + }); + + 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/WrappedTokenBridge.sol b/contracts/core/ccip/WrappedTokenBridge.sol new file mode 100644 index 00000000..a150b2e2 --- /dev/null +++ b/contracts/core/ccip/WrappedTokenBridge.sol @@ -0,0 +1,264 @@ +// 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 "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../interfaces/IWrappedLST.sol"; +import "./base/CCIPReceiver.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 CCIPReceiver { + using SafeERC20 for IERC20; + + IERC20 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 sourceChainSelector, + address indexed sender, + address receiver, + uint256 tokenAmount + ); + + error InvalidSender(); + error InvalidValue(); + error InsufficientFee(); + error TransferFailed(); + error FeeExceedsLimit(); + error InvalidMessage(); + error InvalidMsgValue(); + error InvalidReceiver(); + + /** + * @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, + address _token, + address _wrappedToken + ) CCIPReceiver(_router) { + linkToken = IERC20(_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); + } + + /** + * @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) + **/ + function onTokenTransfer( + address _sender, + uint256 _value, + bytes calldata _calldata + ) external { + if (msg.sender != address(token)) revert InvalidSender(); + if (_value == 0) revert InvalidValue(); + + (uint64 destinationChainSelector, address receiver, uint256 maxLINKFee) = abi.decode( + _calldata, + (uint64, address, uint256) + ); + _transferTokens(destinationChainSelector, _sender, receiver, _value, false, maxLINKFee); + } + + /** + * @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 + **/ + function transferTokens( + uint64 _destinationChainSelector, + address _receiver, + uint256 _amount, + bool _payNative, + uint256 _maxLINKFee + ) external payable 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); + } + + /** + * @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, + uint256 _amount, + bool _payNative + ) external view returns (uint256) { + Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage( + address(this), + _amount, + _payNative ? address(0) : address(linkToken) + ); + + return IRouterClient(this.getRouter()).getFee(_destinationChainSelector, evm2AnyMessage); + } + + /** + * @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, + uint256[] calldata _amounts, + address _receiver + ) external onlyOwner { + if (_receiver == address(0)) revert InvalidReceiver(); + + for (uint256 i = 0; i < _tokens.length; ++i) { + IERC20(_tokens[i]).safeTransfer(_receiver, _amounts[i]); + } + } + + /** + * @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 + **/ + function _transferTokens( + uint64 _destinationChainSelector, + address _sender, + address _receiver, + uint256 _amount, + bool _payNative, + uint256 _maxLINKFee + ) 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, + _payNative ? address(0) : address(linkToken) + ); + + 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 TokensTransferred( + messageId, + _destinationChainSelector, + _sender, + _receiver, + amountToTransfer, + _payNative ? address(0) : address(linkToken), + fees + ); + 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 + **/ + 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})), + feeToken: _feeTokenAddress + }); + + return evm2AnyMessage; + } + + /** + * @notice Processes a received message + * @param _message CCIP message + **/ + function _ccipReceive(Client.Any2EVMMessage memory _message) internal override { + if (_message.destTokenAmounts.length != 1) revert InvalidMessage(); + + 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(); + + uint256 preUnwrapBalance = token.balanceOf(address(this)); + wrappedToken.unwrap(tokenAmount); + uint256 amountToTransfer = token.balanceOf(address(this)) - preUnwrapBalance; + token.safeTransfer(receiver, amountToTransfer); + + emit TokensReceived( + _message.messageId, + _message.sourceChainSelector, + abi.decode(_message.sender, (address)), + receiver, + tokenAmount + ); + } +} 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 new file mode 100644 index 00000000..2b71767f --- /dev/null +++ b/contracts/core/ccip/base/SDLPoolCCIPController.sol @@ -0,0 +1,163 @@ +// 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 "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import "../../interfaces/IRESDLTokenBridge.sol"; +import "../../interfaces/ISDLPool.sol"; +import "./CCIPReceiver.sol"; + +/** + * @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; + + IERC20 public immutable sdlToken; + address public immutable 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); + + error AlreadyAdded(); + error InvalidDestination(); + error SenderNotAuthorized(); + error FeeExceedsLimit(uint256 fee); + error InvalidReceiver(); + + /** + * @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, + uint256 _maxLINKFee + ) CCIPReceiver(_router) { + linkToken = IERC20(_linkToken); + sdlToken = IERC20(_sdlToken); + sdlPool = _sdlPool; + maxLINKFee = _maxLINKFee; + linkToken.approve(_router, type(uint256).max); + sdlToken.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; + + /** + * @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 + 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); + } + } + + /** + * @notice Processes a received message + * @param _message CCIP message + **/ + 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 { + _ccipReceive(_message); + } + } + + /** + * @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, + uint256[] calldata _amounts, + address _receiver + ) external onlyOwner { + if (_receiver == address(0)) revert InvalidReceiver(); + + for (uint256 i = 0; i < _tokens.length; ++i) { + IERC20(_tokens[i]).safeTransfer(_receiver, _amounts[i]); + } + } + + /** + * @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; + } + + /** + * @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/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/IRewardsPoolController.sol b/contracts/core/interfaces/IRewardsPoolController.sol index 3269845f..ee779481 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; } diff --git a/contracts/core/interfaces/ISDLPool.sol b/contracts/core/interfaces/ISDLPool.sol index f41df6fb..a5910945 100644 --- a/contracts/core/interfaces/ISDLPool.sol +++ b/contracts/core/interfaces/ISDLPool.sol @@ -1,6 +1,32 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.15; -interface ISDLPool { +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); + + function supportedTokens() external view returns (address[] memory); + + function handleOutgoingRESDL( + address _sender, + uint256 _reSDLToken, + address _sdlReceiver + ) external returns (RESDLToken memory); + + function handleIncomingRESDL( + address _receiver, + uint256 _tokenId, + RESDLToken calldata _reSDLToken + ) external; } diff --git a/contracts/core/interfaces/ISDLPoolCCIPController.sol b/contracts/core/interfaces/ISDLPoolCCIPController.sol new file mode 100644 index 00000000..7a97095b --- /dev/null +++ b/contracts/core/interfaces/ISDLPoolCCIPController.sol @@ -0,0 +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 _tokenId + ) external returns (address destination, ISDLPool.RESDLToken memory reSDLToken); + + function handleIncomingRESDL( + uint64 _sourceChainSelector, + address _receiver, + 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/interfaces/ISDLPoolCCIPControllerPrimary.sol b/contracts/core/interfaces/ISDLPoolCCIPControllerPrimary.sol new file mode 100644 index 00000000..54a95fb5 --- /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(uint256[] calldata _gasLimits) external; +} 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 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 new file mode 100644 index 00000000..3f6caccd --- /dev/null +++ b/contracts/core/sdlPool/SDLPoolPrimary.sol @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.15; + +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 SDLPool { + using SafeERC20Upgradeable for IERC20Upgradeable; + + address public delegatorPool; + + event IncomingUpdate(uint256 numNewRESDLTokens, int256 totalRESDLSupplyChange, uint256 mintStartIndex); + + /// @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 reinitializer(2) { + if (ccipController == address(0)) { + __SDLPoolBase_init(_name, _symbol, _sdlToken, _boostController); + } else { + delegatorPool = ccipController; + delete ccipController; + } + } + + /** + * @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) { + _storeUpdatedLock(_sender, lockId, _value, lockingDuration); + } else { + _storeNewLock(_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(); + _storeUpdatedLock(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) { + if (locks[_lockId].expiry != 0) revert UnlockAlreadyInitiated(); + uint64 halfDuration = locks[_lockId].duration / 2; + if (locks[_lockId].startTime + halfDuration > block.timestamp) revert HalfDurationNotElapsed(); + + uint64 expiry = uint64(block.timestamp) + halfDuration; + locks[_lockId].expiry = expiry; + + uint256 boostAmount = locks[_lockId].boostAmount; + locks[_lockId].boostAmount = 0; + effectiveBalances[msg.sender] -= boostAmount; + totalEffectiveBalance -= boostAmount; + + emit InitiateUnlock(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) + { + if (locks[_lockId].startTime != 0) { + uint64 expiry = locks[_lockId].expiry; + if (expiry == 0) revert UnlockNotInitiated(); + if (expiry > block.timestamp) revert TotalDurationNotElapsed(); + } + + uint256 baseAmount = locks[_lockId].amount; + if (_amount > baseAmount) revert InsufficientBalance(); + + emit Withdraw(msg.sender, _lockId, _amount); + + if (_amount == baseAmount) { + delete locks[_lockId]; + delete lockOwners[_lockId]; + balances[msg.sender] -= 1; + if (tokenApprovals[_lockId] != address(0)) delete tokenApprovals[_lockId]; + emit Transfer(msg.sender, address(0), _lockId); + } else { + locks[_lockId].amount = baseAmount - _amount; + } + + effectiveBalances[msg.sender] -= _amount; + totalEffectiveBalance -= _amount; + + 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, + 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; + delete tokenApprovals[_lockId]; + + uint256 totalAmount = lock.amount + lock.boostAmount; + effectiveBalances[_sender] -= totalAmount; + effectiveBalances[ccipController] += totalAmount; + + sdlToken.safeTransfer(_sdlReceiver, lock.amount); + + emit OutgoingRESDL(_sender, _lockId); + + 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 _lock lock + */ + function handleIncomingRESDL( + address _receiver, + uint256 _lockId, + Lock calldata _lock + ) external onlyCCIPController updateRewards(_receiver) updateRewards(ccipController) { + if (lockOwners[_lockId] != address(0)) revert InvalidLockId(); + + locks[_lockId] = Lock(_lock.amount, _lock.boostAmount, _lock.startTime, _lock.duration, _lock.expiry); + lockOwners[_lockId] = _receiver; + balances[_receiver] += 1; + + uint256 totalAmount = _lock.amount + _lock.boostAmount; + effectiveBalances[_receiver] += totalAmount; + effectiveBalances[ccipController] -= totalAmount; + + 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 + updateRewards(ccipController) + returns (uint256) + { + uint256 mintStartIndex; + if (_numNewRESDLTokens != 0) { + mintStartIndex = lastLockId + 1; + lastLockId += _numNewRESDLTokens; + } + + if (_totalRESDLSupplyChange > 0) { + effectiveBalances[ccipController] += uint256(_totalRESDLSupplyChange); + totalEffectiveBalance += uint256(_totalRESDLSupplyChange); + } else if (_totalRESDLSupplyChange < 0) { + effectiveBalances[ccipController] -= uint256(-1 * _totalRESDLSupplyChange); + totalEffectiveBalance -= uint256(-1 * _totalRESDLSupplyChange); + } + + emit IncomingUpdate(_numNewRESDLTokens, _totalRESDLSupplyChange, mintStartIndex); + + return mintStartIndex; + } + + /** + * @notice used by the delegator pool to migrate user stakes to this contract + * @dev + * - creates a new lock to represent the migrated stake + * - reverts if `_lockingDuration` exceeds maximum + * @param _sender owner of lock + * @param _amount amount to stake + * @param _lockingDuration duration of lock + */ + function migrate( + address _sender, + uint256 _amount, + uint64 _lockingDuration + ) external { + if (msg.sender != delegatorPool) revert SenderNotAuthorized(); + sdlToken.safeTransferFrom(delegatorPool, address(this), _amount); + _storeNewLock(_sender, _amount, _lockingDuration); + } + + /** + * @notice stores a new lock + * @param _owner owner of lock + * @param _amount amount to stake + * @param _lockingDuration duration of lock + */ + function _storeNewLock( + address _owner, + uint256 _amount, + uint64 _lockingDuration + ) internal updateRewards(_owner) { + Lock memory lock = _createLock(_amount, _lockingDuration); + uint256 lockId = lastLockId + 1; + + locks[lockId] = lock; + lockOwners[lockId] = _owner; + balances[_owner] += 1; + lastLockId++; + + 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); + } + + /** + * @notice stores an updated lock + * @param _owner owner of lock + * @param _amount amount to stake + * @param _lockingDuration duration of lock + */ + function _storeUpdatedLock( + address _owner, + uint256 _lockId, + uint256 _amount, + uint64 _lockingDuration + ) internal onlyLockOwner(_lockId, _owner) updateRewards(_owner) { + Lock memory lock = _updateLock(locks[_lockId], _amount, _lockingDuration); + + int256 diffTotalAmount = int256(lock.amount + lock.boostAmount) - + int256(locks[_lockId].amount + locks[_lockId].boostAmount); + + if (diffTotalAmount > 0) { + effectiveBalances[_owner] += uint256(diffTotalAmount); + totalEffectiveBalance += uint256(diffTotalAmount); + } else if (diffTotalAmount < 0) { + effectiveBalances[_owner] -= uint256(-1 * diffTotalAmount); + totalEffectiveBalance -= uint256(-1 * diffTotalAmount); + } + + locks[_lockId] = lock; + + 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..fe897b73 --- /dev/null +++ b/contracts/core/sdlPool/SDLPoolSecondary.sol @@ -0,0 +1,562 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity 0.8.15; + +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 SDLPool { + using SafeERC20Upgradeable for IERC20Upgradeable; + + struct NewLockPointer { + uint128 updateBatchIndex; + uint128 index; + } + struct LockUpdate { + uint128 updateBatchIndex; + Lock lock; + } + + mapping(uint256 => LockUpdate[]) internal queuedLockUpdates; + + uint256 public queuedNewLockLimit; + uint256[] internal currentMintLockIdByBatch; + Lock[][] internal queuedNewLocks; + mapping(address => NewLockPointer[]) internal newLocksByOwner; + + uint128 public updateBatchIndex; + uint64 internal updateInProgress; + uint64 internal updateNeeded; + int256 public 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(); + error NoUpdateInProgress(); + error TooManyQueuedLocks(); + error LockWithdrawn(); + + /// @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 + * @param _queuedNewLockLimit max amount of queued new locks an account can have + **/ + function initialize( + string memory _name, + string memory _symbol, + address _sdlToken, + address _boostController, + uint256 _queuedNewLockLimit + ) public initializer { + __SDLPoolBase_init(_name, _symbol, _sdlToken, _boostController); + updateBatchIndex = 1; + currentMintLockIdByBatch.push(0); + queuedNewLocks.push(); + queuedNewLocks.push(); + queuedNewLockLimit = _queuedNewLockLimit; + } + + /** + * @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 + * + * 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 + * @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 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 + * - 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 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 + * - 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 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) + * - 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); + } + + /** + * @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); + _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, + 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; + delete tokenApprovals[_lockId]; + + uint256 totalAmount = lock.amount + lock.boostAmount; + effectiveBalances[_sender] -= totalAmount; + totalEffectiveBalance -= totalAmount; + + sdlToken.safeTransfer(_sdlReceiver, lock.amount); + + emit OutgoingRESDL(_sender, _lockId); + + 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 _lock lock + **/ + function handleIncomingRESDL( + address _receiver, + uint256 _lockId, + Lock calldata _lock + ) external onlyCCIPController updateRewards(_receiver) { + if (lockOwners[_lockId] != address(0)) revert InvalidLockId(); + + locks[_lockId] = Lock(_lock.amount, _lock.boostAmount, _lock.startTime, _lock.duration, _lock.expiry); + lockOwners[_lockId] = _receiver; + balances[_receiver] += 1; + + uint256 totalAmount = _lock.amount + _lock.boostAmount; + 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(); + + uint256 numNewQueuedLocks = queuedNewLocks[updateBatchIndex].length; + int256 reSDLSupplyChange = queuedRESDLSupplyChange; + + queuedRESDLSupplyChange = 0; + 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 { + 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 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 + * @param _amount amount of underlying SDL + * @param _lockingDuration locking duration + **/ + function _queueNewLock( + address _owner, + 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))); + queuedRESDLSupplyChange += int256(lock.amount + lock.boostAmount); + if (updateNeeded == 0) updateNeeded = 1; + + emit QueueCreateLock(_owner, _amount, lock.boostAmount, _lockingDuration); + } + + /** + * @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) { + 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) { + newLocksByOwner[_owner].pop(); + } else { + newLocksByOwner[_owner][j] = newLocksByOwner[_owner][i]; + ++i; + } + } + } + + /** + * @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, + uint256 _amount, + 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 += + 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); + } + + /** + * @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) { + uint256 finalizedBatchIndex = _getFinalizedUpdateBatchIndex(); + + for (uint256 i = 0; 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; + 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 && updateLockState.boostAmount == 0) { + locks[lockId].expiry = updateLockState.expiry; + locks[lockId].boostAmount = 0; + emit InitiateUnlock(_owner, lockId, updateLockState.expiry); + } else { + locks[lockId] = updateLockState; + int256 totalDiff = baseAmountDiff + boostAmountDiff; + effectiveBalances[_owner] = uint256(int256(effectiveBalances[_owner]) + totalDiff); + totalEffectiveBalance = uint256(int256(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) { + queuedLockUpdates[lockId].pop(); + } else { + queuedLockUpdates[lockId][k] = queuedLockUpdates[lockId][j]; + ++j; + } + } + } + } + + /** + * @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; + + if (updatesLength != 0) { + return queuedLockUpdates[_lockId][updatesLength - 1].lock; + } else { + return locks[_lockId]; + } + } + + /** + * @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, + uint256 _lockId + ) internal override { + if (queuedLockUpdates[_lockId].length != 0) revert CannotTransferWithQueuedUpdates(); + super._transfer(_from, _to, _lockId); + } +} diff --git a/contracts/core/sdlPool/SDLPool.sol b/contracts/core/sdlPool/base/SDLPool.sol similarity index 63% rename from contracts/core/sdlPool/SDLPool.sol rename to contracts/core/sdlPool/base/SDLPool.sol index 9356c530..eaff71af 100644 --- a/contracts/core/sdlPool/SDLPool.sol +++ b/contracts/core/sdlPool/base/SDLPool.sol @@ -4,13 +4,13 @@ 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/RewardsPoolController.sol"; +import "../../interfaces/IBoostController.sol"; +import "../../interfaces/IERC721Receiver.sol"; /** * @title SDL Pool - * @notice Allows users to stake/lock SDL tokens and receive a percentage of the protocol's earned rewards + * @notice Base SDL Pool contract to inherit from */ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUpgradeable { using SafeERC20Upgradeable for IERC20Upgradeable; @@ -26,24 +26,26 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp string public name; string public symbol; - mapping(address => mapping(address => bool)) private operatorApprovals; - mapping(uint256 => address) private tokenApprovals; + mapping(address => mapping(address => bool)) internal operatorApprovals; + mapping(uint256 => address) internal 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; + mapping(uint256 => Lock) internal locks; + mapping(uint256 => address) internal lockOwners; + mapping(address => uint256) internal balances; uint256 public totalEffectiveBalance; - mapping(address => uint256) private effectiveBalances; + mapping(address => uint256) internal effectiveBalances; - address public delegatorPool; + address public ccipController; 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( @@ -60,17 +62,19 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp 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 InvalidValue(); error InvalidLockingDuration(); - error InvalidParams(); error TransferFromIncorrectOwner(); - error TransferToZeroAddress(); + error TransferToInvalidAddress(); error TransferToNonERC721Implementer(); error ApprovalToCurrentOwner(); error ApprovalToCaller(); + error InvalidValue(); + error InvalidParams(); error UnauthorizedToken(); error TotalDurationNotElapsed(); error HalfDurationNotElapsed(); @@ -80,38 +84,39 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp error ContractNotFound(); error UnlockAlreadyInitiated(); - /// @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 - * @param _delegatorPool address of the old contract this one will replace **/ - function initialize( + function __SDLPoolBase_init( string memory _name, string memory _symbol, address _sdlToken, - address _boostController, - address _delegatorPool - ) public initializer { + address _boostController + ) public onlyInitializing { __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(); + _onlyLockOwner(_lockId, _owner); + _; + } + + /** + * @notice reverts if sender is not the CCIP controller + **/ + modifier onlyCCIPController() { + if (msg.sender != ccipController) revert SenderNotAuthorized(); _; } @@ -189,129 +194,6 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp return lockIds; } - /** - * @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) { - _updateLock(_sender, lockId, _value, lockingDuration); - } else { - _createLock(_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(); - _updateLock(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) { - if (locks[_lockId].expiry != 0) revert UnlockAlreadyInitiated(); - uint64 halfDuration = locks[_lockId].duration / 2; - if (locks[_lockId].startTime + halfDuration > block.timestamp) revert HalfDurationNotElapsed(); - - uint64 expiry = uint64(block.timestamp) + halfDuration; - locks[_lockId].expiry = expiry; - - uint256 boostAmount = locks[_lockId].boostAmount; - locks[_lockId].boostAmount = 0; - effectiveBalances[msg.sender] -= boostAmount; - totalEffectiveBalance -= boostAmount; - - emit InitiateUnlock(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) - { - if (locks[_lockId].startTime != 0) { - uint64 expiry = locks[_lockId].expiry; - if (expiry == 0) revert UnlockNotInitiated(); - if (expiry > block.timestamp) revert TotalDurationNotElapsed(); - } - - uint256 baseAmount = locks[_lockId].amount; - if (_amount > baseAmount) revert InsufficientBalance(); - - emit Withdraw(msg.sender, _lockId, _amount); - - if (_amount == baseAmount) { - delete locks[_lockId]; - delete lockOwners[_lockId]; - balances[msg.sender] -= 1; - if (tokenApprovals[_lockId] != address(0)) delete tokenApprovals[_lockId]; - emit Transfer(msg.sender, address(0), _lockId); - } else { - locks[_lockId].amount = baseAmount - _amount; - } - - effectiveBalances[msg.sender] -= _amount; - totalEffectiveBalance -= _amount; - - sdlToken.safeTransfer(msg.sender, _amount); - } - /** * @notice transfers a lock between accounts * @dev reverts if sender is not the owner of and not approved to transfer the lock @@ -447,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 @@ -483,51 +375,25 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp } /** - * @notice used by the delegator pool to migrate user stakes to this contract - * @dev - * - creates a new lock to represent the migrated stake - * - reverts if `_lockingDuration` exceeds maximum - * @param _sender owner of lock - * @param _amount amount to stake - * @param _lockingDuration duration of lock + * @notice sets the CCIP controller + * @dev this contract interfaces with CCIP + * @param _ccipController address of CCIP controller */ - function migrate( - address _sender, - uint256 _amount, - uint64 _lockingDuration - ) external { - if (msg.sender != delegatorPool) revert SenderNotAuthorized(); - sdlToken.safeTransferFrom(delegatorPool, address(this), _amount); - _createLock(_sender, _amount, _lockingDuration); + function setCCIPController(address _ccipController) external onlyOwner { + ccipController = _ccipController; } /** * @notice creates a new lock * @dev reverts if `_lockingDuration` exceeds maximum - * @param _sender owner of lock * @param _amount amount to stake * @param _lockingDuration duration of lock */ - function _createLock( - address _sender, - uint256 _amount, - uint64 _lockingDuration - ) private updateRewards(_sender) { + function _createLock(uint256 _amount, uint64 _lockingDuration) internal view returns (Lock memory) { uint256 boostAmount = boostController.getBoostAmount(_amount, _lockingDuration); - uint256 totalAmount = _amount + boostAmount; uint64 startTime = _lockingDuration != 0 ? uint64(block.timestamp) : 0; - uint256 lockId = lastLockId + 1; - - locks[lockId] = Lock(_amount, boostAmount, startTime, _lockingDuration, 0); - lockOwners[lockId] = _sender; - balances[_sender] += 1; - lastLockId++; - - effectiveBalances[_sender] += totalAmount; - totalEffectiveBalance += totalAmount; - emit CreateLock(_sender, lockId, _amount, boostAmount, _lockingDuration); - emit Transfer(address(0), _sender, lockId); + return Lock(_amount, boostAmount, startTime, _lockingDuration, 0); } /** @@ -536,58 +402,46 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp * - 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 _lock lock to update * @param _amount additional amount to stake * @param _lockingDuration duration of lock */ function _updateLock( - address _sender, - uint256 _lockId, + Lock memory _lock, 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) { + ) internal view returns (Lock memory) { + if ((_lock.expiry == 0 || _lock.expiry > block.timestamp) && _lockingDuration < _lock.duration) { revert InvalidLockingDuration(); } - uint256 curBaseAmount = locks[_lockId].amount; + Lock memory lock = Lock(_lock.amount, _lock.boostAmount, _lock.startTime, _lock.duration, _lock.expiry); - uint256 baseAmount = curBaseAmount + _amount; + uint256 baseAmount = _lock.amount + _amount; uint256 boostAmount = boostController.getBoostAmount(baseAmount, _lockingDuration); - 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; + lock.startTime = uint64(block.timestamp); + } else { + delete lock.startTime; } - int256 diffTotalAmount = int256(baseAmount + boostAmount) - int256(curBaseAmount + locks[_lockId].boostAmount); - if (diffTotalAmount > 0) { - effectiveBalances[_sender] += uint256(diffTotalAmount); - totalEffectiveBalance += uint256(diffTotalAmount); - } else if (diffTotalAmount < 0) { - effectiveBalances[_sender] -= uint256(-1 * diffTotalAmount); - totalEffectiveBalance -= uint256(-1 * diffTotalAmount); - } + lock.amount = baseAmount; + lock.boostAmount = boostAmount; + lock.duration = _lockingDuration; + lock.expiry = 0; - locks[_lockId].boostAmount = boostAmount; + return lock; + } - emit UpdateLock(_sender, _lockId, baseAmount, boostAmount, _lockingDuration); + /** + * @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(); } /** @@ -603,9 +457,9 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp address _from, address _to, uint256 _lockId - ) private { + ) internal virtual { if (_from != ownerOf(_lockId)) revert TransferFromIncorrectOwner(); - if (_to == address(0)) revert TransferToZeroAddress(); + if (_to == address(0) || _to == ccipController || _to == _from) revert TransferToInvalidAddress(); delete tokenApprovals[_lockId]; @@ -640,7 +494,7 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp address _to, uint256 _lockId, bytes memory _data - ) private returns (bool) { + ) 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; @@ -666,7 +520,7 @@ contract SDLPool is RewardsPoolController, IERC721Upgradeable, IERC721MetadataUp * @param _lockId id of lock * @return whether address is authorized ot not **/ - function _isApprovedOrOwner(address _spender, uint256 _lockId) private view returns (bool) { + function _isApprovedOrOwner(address _spender, uint256 _lockId) internal view returns (bool) { address owner = ownerOf(_lockId); return (_spender == owner || isApprovedForAll(owner, _spender) || getApproved(_lockId) == _spender); } diff --git a/contracts/core/test/SDLPoolCCIPControllerMock.sol b/contracts/core/test/SDLPoolCCIPControllerMock.sol new file mode 100644 index 00000000..c213caef --- /dev/null +++ b/contracts/core/test/SDLPoolCCIPControllerMock.sol @@ -0,0 +1,54 @@ +// 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; + + uint256 public rewardsDistributed; + + error OnlyRESDLTokenBridge(); + + modifier onlyBridge() { + if (msg.sender != reSDLTokenBridge) revert OnlyRESDLTokenBridge(); + _; + } + + constructor(address _sdlToken, address _sdlPool) { + sdlToken = IERC20(_sdlToken); + sdlPool = ISDLPool(_sdlPool); + } + + function handleOutgoingRESDL( + uint64, + address _sender, + uint256 _tokenId + ) external onlyBridge returns (address, ISDLPool.RESDLToken memory) { + return (address(0), sdlPool.handleOutgoingRESDL(_sender, _tokenId, reSDLTokenBridge)); + } + + function handleIncomingRESDL( + uint64, + address _receiver, + uint256 _tokenId, + ISDLPool.RESDLToken calldata _reSDLToken + ) external onlyBridge { + sdlToken.safeTransferFrom(reSDLTokenBridge, address(sdlPool), _reSDLToken.amount); + sdlPool.handleIncomingRESDL(_receiver, _tokenId, _reSDLToken); + } + + function distributeRewards(uint256[] calldata) external { + rewardsDistributed++; + } + + function setRESDLTokenBridge(address _reSDLTokenBridge) external { + reSDLTokenBridge = _reSDLTokenBridge; + } +} 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..b79ed557 --- /dev/null +++ b/contracts/core/test/chainlink/CCIPOffRampMock.sol @@ -0,0 +1,57 @@ +// 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 returns (bool) { + for (uint256 i = 0; i < _tokenAmounts.length; ++i) { + tokenPools[_tokenAmounts[i].token].releaseOrMint(_receiver, _tokenAmounts[i].amount); + } + + (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 { + tokenPools[_token] = ITokenPool(_pool); + } +} diff --git a/contracts/core/test/chainlink/CCIPOnRampMock.sol b/contracts/core/test/chainlink/CCIPOnRampMock.sol new file mode 100644 index 00000000..6f6ef1c7 --- /dev/null +++ b/contracts/core/test/chainlink/CCIPOnRampMock.sol @@ -0,0 +1,63 @@ +// 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 RequestData { + uint256 feeTokenAmount; + address originalSender; + } + + mapping(address => address) public tokenPools; + address public linkToken; + + Client.EVM2AnyMessage[] public requestMessages; + RequestData[] public requestData; + + 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 requestMessages[requestMessages.length - 1]; + } + + function getLastRequestData() external view returns (RequestData memory) { + return requestData[requestData.length - 1]; + } + + function getFee(uint64, Client.EVM2AnyMessage calldata _message) external view returns (uint256) { + return _message.feeToken == linkToken ? 2 ether : 3 ether; + } + + function getPoolBySourceToken(uint64, address _token) public view returns (address) { + return tokenPools[_token]; + } + + function forwardFromRouter( + uint64, + Client.EVM2AnyMessage calldata _message, + uint256 _feeTokenAmount, + address _originalSender + ) external returns (bytes32) { + 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/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..863d5457 --- /dev/null +++ b/contracts/core/test/chainlink/CLContractImports0.8.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: GPL-3.0 +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/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/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 { diff --git a/deployments/mainnet.json b/deployments/mainnet.json index 1eceb501..a082f1c1 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", @@ -170,5 +170,9 @@ "LINK_PP_DistributionOracle": { "address": "0x2285AC429cCCAaE7cC1E27BfBe617bC626B443CF", "artifact": "DistributionOracle" + }, + "stLINK_WrappedTokenBridge": { + "address": "0x6C1E2D2c55C83De945e3f37dF694cdE8452C1E82", + "artifact": "WrappedTokenBridge" } } 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/hardhat.config.ts b/hardhat.config.ts index 1d2128f9..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: { @@ -63,7 +64,7 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: '0.8.15', + version: '0.8.19', settings: { optimizer: { enabled: true, @@ -71,6 +72,15 @@ const config: HardhatUserConfig = { }, }, }, + { + version: '0.8.15', + settings: { + optimizer: { + enabled: true, + runs: 115, + }, + }, + }, { version: '0.7.6', settings: { diff --git a/package.json b/package.json index 3f394610..92a83a50 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": "^1.2.1", "@openzeppelin/contracts": "^4.7.0", "@openzeppelin/contracts-upgradeable": "^4.9.2", "@prb/math": "^2.5.0", diff --git a/scripts/prod/ccip/stage-1/deploy-ccip-dest-tokens.ts b/scripts/prod/ccip/stage-1/deploy-ccip-dest-tokens.ts new file mode 100644 index 00000000..7c2b443c --- /dev/null +++ b/scripts/prod/ccip/stage-1/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) + }) diff --git a/scripts/prod/ccip/stage-1/deploy-ccip-stage-1.ts b/scripts/prod/ccip/stage-1/deploy-ccip-stage-1.ts new file mode 100644 index 00000000..11b024e1 --- /dev/null +++ b/scripts/prod/ccip/stage-1/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) + }) 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/test/deploy-test-contracts.ts b/scripts/test/deploy-test-contracts.ts index ce3d2658..0e22007c 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( { @@ -59,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/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 new file mode 100644 index 00000000..d8473e23 --- /dev/null +++ b/scripts/test/setup-testnet.ts @@ -0,0 +1,140 @@ +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('contracts/core/tokens/base/ERC677.sol: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: 'contracts/core/tokens/base/ERC677.sol: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) + }) 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 } 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 new file mode 100644 index 00000000..38ba73e3 --- /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, + SDLPoolPrimary, + SDLPoolCCIPControllerPrimary, +} 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: SDLPoolPrimary + let sdlPoolCCIPController: SDLPoolCCIPControllerPrimary + 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('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') + 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', [10, 4 * 365 * 86400, 4]) + sdlPool = (await deployUpgradeable('SDLPoolPrimary', [ + 'reSDL', + 'reSDL', + sdlToken.address, + boostController.address, + ])) as SDLPoolPrimary + sdlPoolCCIPController = (await deploy('SDLPoolCCIPControllerPrimary', [ + router.address, + linkToken.address, + sdlToken.address, + sdlPool.address, + toEther(10), + accounts[0], + ])) as SDLPoolCCIPControllerPrimary + + bridge = (await deploy('RESDLTokenBridge', [ + 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 sdlPoolCCIPController.addWhitelistedChain(77, accounts[6]) + 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, 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 () => { + 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), 1) + let lastRequestData = await onRamp.getLastRequestData() + 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], sdlPoolCCIPController.address) + + assert.equal( + ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], + accounts[6] + ) + 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) + 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), 1)).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), 1) + lastRequestData = await onRamp.getLastRequestData() + 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], sdlPoolCCIPController.address) + + assert.equal( + ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], + accounts[6] + ) + 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) + assert.equal( + lastRequestMsg[4], + '0x97a657c9' + ethers.utils.defaultAbiCoder.encode(['uint256'], [1]).slice(2) + ) + await expect(sdlPool.ownerOf(3)).to.be.revertedWith('InvalidLockId()') + }) + + 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]) + + await bridge.transferRESDL(77, accounts[4], 2, true, toEther(10), 1, { value: toEther(10) }) + let lastRequestData = await onRamp.getLastRequestData() + 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], sdlPoolCCIPController.address) + + assert.equal( + ethers.utils.defaultAbiCoder.decode(['address'], lastRequestMsg[0])[0], + accounts[6] + ) + 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) + 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), 1) + ).to.be.revertedWith('SenderNotAuthorized()') + await expect( + 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), 1) + ).to.be.revertedWith('InvalidDestination()') + + 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), 1, { value: toEther(10) }) + + let success: any = await offRamp + .connect(signers[1]) + .callStatic.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(success, false) + + 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]) + 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, + }, + ] + ) + }) +}) 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..007d1fdb --- /dev/null +++ b/test/core/ccip/sdl-pool-ccip-controller-primary.test.ts @@ -0,0 +1,479 @@ +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('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 + 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', [10, 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), + accounts[0], + ])) 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]) + await controller.setRewardsInitiator(accounts[0]) + await controller.addWhitelistedChain(77, accounts[4]) + }) + + 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( + 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(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 controller.connect(signers[5]).handleOutgoingRESDL(77, accounts[0], 1) + + 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]) + 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(88, accounts[6]) + + assert.deepEqual( + (await controller.getWhitelistedChains()).map((d) => d.toNumber()), + [77, 88] + ) + assert.equal(await controller.whitelistedDestinations(77), accounts[4]) + assert.equal(await controller.whitelistedDestinations(88), accounts[6]) + + await expect(controller.addWhitelistedChain(77, accounts[7])).to.be.revertedWith( + 'AlreadyAdded()' + ) + await expect( + controller.addWhitelistedChain(99, ethers.constants.AddressZero) + ).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) + + 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.connect(signers[5]).handleOutgoingRESDL(77, accounts[0], 1) + await token1.transferAndCall(rewardsPool1.address, toEther(50), '0x') + await controller.distributeRewards([1, 2]) + + 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(requestMsg[3], linkToken.address) + 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]] + ) + 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]) + 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([3, 4]) + + 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[4]) + assert.equal(requestMsg[3], linkToken.address) + 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])]), + [ + [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.equal( + requestMsg[4], + '0x97a657c9' + ethers.utils.defaultAbiCoder.encode(['uint256'], [4]).slice(2) + ) + 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 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([1, 2]) + + 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(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 offRamp + .connect(signers[4]) + .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) + assert.deepEqual( + await controller + .getQueuedUpdates() + .then((d) => d.map((v) => [v[0].toNumber(), v[1].toNumber()])), + [[77, 3]] + ) + + await offRamp + .connect(signers[4]) + .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) + assert.deepEqual( + await controller + .getQueuedUpdates() + .then((d) => d.map((v) => [v[0].toNumber(), v[1].toNumber()])), + [ + [77, 3], + [77, 0], + ] + ) + + 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], 3) + assert.equal(requestMsg[3], linkToken.address) + 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]) + 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, + [] + ) + + await controller.executeQueuedUpdates([1, 2]) + + assert.deepEqual(await controller.getQueuedUpdates(), []) + + 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) + 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 () => { + await linkToken.transfer(controller.address, toEther(1000)) + await sdlToken.transfer(controller.address, toEther(2000)) + await controller.recoverTokens( + [linkToken.address, sdlToken.address], + [toEther(1000), toEther(2000)], + 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 new file mode 100644 index 00000000..79543dde --- /dev/null +++ b/test/core/ccip/sdl-pool-ccip-controller-secondary.test.ts @@ -0,0 +1,396 @@ +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 { Signer } from 'ethers' +import { time } from '@nomicfoundation/hardhat-network-helpers' + +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: any + 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('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]) + 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', [10, 4 * 365 * 86400, 4]) + sdlPool = (await deployUpgradeable('SDLPoolSecondary', [ + 'reSDL', + 'reSDL', + sdlToken.address, + boostController.address, + 5, + ])) as SDLPoolSecondary + controller = (await deploy('SDLPoolCCIPControllerSecondary', [ + router.address, + linkToken.address, + sdlToken.address, + sdlPool.address, + 77, + accounts[4], + toEther(10), + accounts[0], + 100, + ])) 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(77, accounts[0], 2) + ).to.be.revertedWith('SenderNotAuthorized()') + + assert.deepEqual( + 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(controller.address)), 200) + await expect(sdlPool.ownerOf(2)).to.be.revertedWith('InvalidLockId()') + }) + + it('handleIncomingRESDL should work correctly', async () => { + 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(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]), { + amount: 300, + boostAmount: 200, + startTime: 111, + duration: 222, + expiry: 0, + }) + }) + + it('shouldUpdate should work correctly', async () => { + 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.shouldUpdate(), true) + + await controller.executeUpdate(1) + 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.shouldUpdate(), false) + + await time.increase(100) + + assert.equal(await controller.shouldUpdate(), false) + }) + + 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 controller.executeUpdate(1) + await expect(controller.executeUpdate(1)).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) + assert.equal( + lastRequestMsg[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'], [3]), + controller.address, + [] + ) + await expect(controller.executeUpdate(1)).to.be.revertedWith('UpdateConditionsNotMet()') + + await sdlPool.connect(signers[1]).withdraw(2, toEther(10)) + await expect(controller.executeUpdate(1)).to.be.revertedWith('UpdateConditionsNotMet()') + + await time.increase(100) + await controller.executeUpdate(2) + + 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) + assert.equal( + lastRequestMsg[4], + '0x97a657c9' + ethers.utils.defaultAbiCoder.encode(['uint256'], [2]).slice(2) + ) + }) + + 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) + + let success: any = await offRamp + .connect(signers[4]) + .callStatic.executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + '0x', + controller.address, + [ + { token: token1.address, amount: toEther(25) }, + { token: token2.address, amount: toEther(50) }, + ] + ) + assert.equal(success, false) + + 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) + + 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(), false) + 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] + ) + + 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 sdlToken.transferAndCall( + sdlPool.address, + toEther(300), + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint64'], [0, 0]) + ) + await controller.executeUpdate(1) + + 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( + ethers.utils.formatBytes32String('messageId'), + 77, + ethers.utils.defaultAbiCoder.encode(['uint256'], [7]), + controller.address, + [] + ) + assert.equal(await controller.shouldUpdate(), false) + + await sdlPool.executeQueuedOperations([]) + assert.deepEqual(parseLock((await sdlPool.getLocks([7]))[0]), { + amount: 300, + boostAmount: 0, + 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], + [toEther(1000), toEther(2000)], + 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/wrapped-token-bridge.test.ts b/test/core/ccip/wrapped-token-bridge.test.ts new file mode 100644 index 00000000..b5f5a693 --- /dev/null +++ b/test/core/ccip/wrapped-token-bridge.test.ts @@ -0,0 +1,284 @@ +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('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, + '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 stakingPool.setRewardsInitiator(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, 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 () => { + let preFeeBalance = await linkToken.balanceOf(accounts[0]) + + await bridge.transferTokens(77, accounts[4], toEther(100), false, toEther(10)) + let lastRequestData = await onRamp.getLastRequestData() + 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)) + ).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, { + value: toEther(10), + }) + let lastRequestData = await onRamp.getLastRequestData() + 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.getLastRequestData() + 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) + + await token2.transfer(tokenPool2.address, toEther(100)) + + 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(25) }, + ] + ) + 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(25) }] + ) + assert.equal(success, false) + + success = await offRamp.callStatic.executeSingleMessage( + ethers.utils.formatBytes32String('messageId'), + 77, + '0x', + bridge.address, + [{ token: wrappedToken.address, amount: toEther(25) }] + ) + assert.equal(success, false) + }) + + 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], + [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/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 664e2802..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', [ @@ -61,6 +65,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 59% rename from test/core/slashing-keeper.test.ts rename to test/core/rewards-initiator.test.ts index eaaca828..5ea20783 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 @@ -36,7 +38,11 @@ describe('SlashingKeeper', () => { }) 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', [ @@ -46,7 +52,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 +91,8 @@ describe('SlashingKeeper', () => { await stakingPool.addStrategy(strategy2.address) 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)) @@ -85,27 +101,25 @@ 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()), - [2], - 'performData incorrect' + [2] ) 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()), - [0, 2], - 'performData incorrect' + [0, 2] ) }) @@ -114,9 +128,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()), @@ -136,11 +150,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/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/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.test.ts b/test/core/sdlPool/sdl-pool-primary.test.ts similarity index 82% rename from test/core/sdlPool/sdl-pool.test.ts rename to test/core/sdlPool/sdl-pool-primary.test.ts index 01503ae0..bae949f1 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[] @@ -46,30 +44,27 @@ describe('SDLPool', () => { 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) - delegatorPool = (await deployUpgradeable('DelegatorPool', [ - sdlToken.address, - 'Staked SDL', - 'stSDL', - [], - ])) as DelegatorPool - boostController = (await deploy('LinearBoostController', [ + 10, 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 +72,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 +1109,218 @@ 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) + + 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 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) + 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') - await expect(delegatorPool.migrate(toEther(200), 0)).to.be.revertedWith('Insufficient balance') - await expect(delegatorPool.migrate(0, 0)).to.be.revertedWith('Invalid amount') + let rewards1 = await rewardsPool.withdrawableRewards(accounts[1]) + let rewards2 = await rewardsPool.withdrawableRewards(accounts[0]) - 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.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, { + 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, { + amount: toEther(100), + boostAmount: toEther(50), + startTime: 123, + duration: 456, + expiry: 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 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, + 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, { + 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)) + }) + + 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) + }) + + 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('TransferToInvalidAddress()') }) }) 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..35dbacab --- /dev/null +++ b/test/core/sdlPool/sdl-pool-secondary.test.ts @@ -0,0 +1,1945 @@ +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('contracts/core/tokens/base/ERC677.sol:ERC677', [ + 'Chainlink', + 'LINK', + 1000000000, + ])) as ERC677 + + await sdlToken.mint(accounts[0], toEther(1000000)) + await setupToken(sdlToken, accounts) + + boostController = (await deploy('LinearBoostController', [ + 10, + 4 * 365 * DAY, + 4, + ])) as LinearBoostController + + sdlPool = (await deployUpgradeable('SDLPoolSecondary', [ + 'Reward Escrowed SDL', + 'reSDL', + sdlToken.address, + boostController.address, + 5, + ])) 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, { + amount: toEther(100), + boostAmount: toEther(50), + startTime: 123, + duration: 456, + expiry: 789, + }) + ).to.be.revertedWith('InvalidLockId()') + + 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, + 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, { + amount: toEther(200), + boostAmount: toEther(400), + startTime: 1, + duration: 2, + expiry: 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, { + 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)) + }) + + 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.isUpdateInProgress(), true) + 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.isUpdateInProgress(), true) + 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 }, + ]) + }) + + 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..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 @@ -76,6 +80,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/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 18ff5f50..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', [ @@ -56,6 +60,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/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-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..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', [ @@ -70,6 +74,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) 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, diff --git a/yarn.lock b/yarn.lock index f0d6e28d..636cf8fb 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@^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" - "@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" + +"@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" "npm:@openzeppelin/contracts-upgradeable@v4.7.3" "@openzeppelin/contracts-v0.7" "npm:@openzeppelin/contracts@v3.4.2" "@cspotcode/source-map-consumer@0.8.0": @@ -240,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" @@ -399,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== @@ -442,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" @@ -479,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" @@ -512,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" @@ -541,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" @@ -563,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" @@ -580,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" @@ -605,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" @@ -626,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" @@ -656,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" @@ -702,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" @@ -749,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" @@ -786,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" @@ -813,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" @@ -834,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" @@ -851,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" @@ -873,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" @@ -888,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" @@ -927,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" @@ -961,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" @@ -977,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" @@ -1002,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" @@ -1032,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" @@ -1068,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" @@ -1101,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" @@ -1134,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== @@ -1173,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" @@ -1203,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" @@ -1235,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" @@ -1268,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" @@ -1615,10 +1334,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" @@ -2295,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" @@ -3329,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" @@ -3495,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" @@ -3656,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== @@ -3702,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" @@ -4226,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" @@ -5249,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" @@ -5820,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" @@ -6191,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" @@ -7293,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" @@ -7564,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" @@ -9887,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==