-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Rebalancer #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: Rebalancer #16
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
6024a1e
Add liquidity mining and partial tests
lastperson 78aaa2e
Add liquidity mining tests
lastperson fe1d37b
Add default .env to CI
lastperson 163aa01
WIP rebalancer
lastperson 9bf8268
Finish implementation
lastperson f2bc34d
Merge branch 'main' into feat-rebalancer
lastperson 71ffb60
Add tests and deployment
lastperson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
| pragma solidity 0.8.28; | ||
|
|
||
| import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
| import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; | ||
| import {AccessControlUpgradeable} from '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol'; | ||
| import {ERC7201Helper} from './utils/ERC7201Helper.sol'; | ||
| import {ILiquidityPool} from './interfaces/ILiquidityPool.sol'; | ||
| import {IRebalancer} from './interfaces/IRebalancer.sol'; | ||
| import {ICCTPTokenMessenger, ICCTPMessageTransmitter} from './interfaces/ICCTP.sol'; | ||
|
|
||
| contract Rebalancer is IRebalancer, AccessControlUpgradeable { | ||
| using BitMaps for BitMaps.BitMap; | ||
|
|
||
| ILiquidityPool immutable public LIQUIDITY_POOL; | ||
| IERC20 immutable public COLLATERAL; | ||
| ICCTPTokenMessenger immutable public CCTP_TOKEN_MESSENGER; | ||
| ICCTPMessageTransmitter immutable public CCTP_MESSAGE_TRANSMITTER; | ||
| bytes32 constant public REBALANCER_ROLE = "REBALANCER_ROLE"; | ||
|
|
||
| event InitiateRebalance(uint256 amount, Domain destinationDomain, Provider provider); | ||
| event ProcessRebalance(Provider provider); | ||
| event SetRoute(Domain destinationDomain, Provider provider, bool isAllowed); | ||
|
|
||
| error ZeroAddress(); | ||
| error ZeroAmount(); | ||
| error RouteDenied(); | ||
| error ProcessFailed(); | ||
| error UnsupportedDomain(); | ||
| error UnsupportedProvider(); | ||
| error InvalidLength(); | ||
|
|
||
| /// @custom:storage-location erc7201:sprinter.storage.Rebalancer | ||
| struct RebalancerStorage { | ||
| BitMaps.BitMap allowedRoutes; | ||
| } | ||
|
|
||
| bytes32 private constant StorageLocation = 0x81fbb040176d3bdbf3707b380997ee0038798f9e3ad0bae77fff3621ef225c00; | ||
|
|
||
| constructor( | ||
| address liquidityPool, | ||
| address cctpTokenMessenger, | ||
| address cctpMessageTransmitter | ||
| ) { | ||
| ERC7201Helper.validateStorageLocation( | ||
| StorageLocation, | ||
| 'sprinter.storage.Rebalancer' | ||
| ); | ||
| if (liquidityPool == address(0)) revert ZeroAddress(); | ||
| if (cctpTokenMessenger == address(0)) revert ZeroAddress(); | ||
| if (cctpMessageTransmitter == address(0)) revert ZeroAddress(); | ||
| LIQUIDITY_POOL = ILiquidityPool(liquidityPool); | ||
| COLLATERAL = ILiquidityPool(liquidityPool).COLLATERAL(); | ||
| CCTP_TOKEN_MESSENGER = ICCTPTokenMessenger(cctpTokenMessenger); | ||
| CCTP_MESSAGE_TRANSMITTER = ICCTPMessageTransmitter(cctpMessageTransmitter); | ||
|
|
||
| _disableInitializers(); | ||
| } | ||
|
|
||
| function initialize( | ||
| address admin, | ||
| address rebalancer, | ||
| Domain[] memory domains, | ||
| Provider[] memory providers | ||
| ) external initializer() { | ||
| _grantRole(DEFAULT_ADMIN_ROLE, admin); | ||
| _grantRole(REBALANCER_ROLE, rebalancer); | ||
| _setRoute(domains, providers, true); | ||
| } | ||
|
|
||
| function setRoute( | ||
| Domain[] calldata domains, | ||
| Provider[] calldata providers, | ||
| bool isAllowed | ||
| ) external onlyRole(DEFAULT_ADMIN_ROLE) { | ||
| _setRoute(domains, providers, isAllowed); | ||
| } | ||
|
|
||
| function _setRoute(Domain[] memory domains, Provider[] memory providers, bool isAllowed) internal { | ||
| RebalancerStorage storage $ = _getStorage(); | ||
| require(domains.length == providers.length, InvalidLength()); | ||
| for (uint256 i = 0; i < domains.length; ++i) { | ||
| Domain domain = domains[i]; | ||
| Provider provider = providers[i]; | ||
| $.allowedRoutes.setTo(_toIndex(domain, provider), isAllowed); | ||
| emit SetRoute(domain, provider, isAllowed); | ||
| } | ||
| } | ||
|
|
||
| function _toIndex(Domain domain, Provider provider) internal pure returns (uint256) { | ||
| return (uint256(domain) << 8) + uint256(provider); | ||
| } | ||
|
|
||
| function isRouteAllowed(Domain domain, Provider provider) public view returns (bool) { | ||
| return _getStorage().allowedRoutes.get(_toIndex(domain, provider)); | ||
| } | ||
|
|
||
| function initiateRebalance( | ||
| uint256 amount, | ||
| Domain destinationDomain, | ||
| Provider provider, | ||
| bytes calldata /*extraData*/ | ||
| ) external override onlyRole(REBALANCER_ROLE) { | ||
| require(amount > 0, ZeroAmount()); | ||
| require(isRouteAllowed(destinationDomain, provider), RouteDenied()); | ||
|
|
||
| LIQUIDITY_POOL.withdraw(address(this), amount); | ||
| if (provider == Provider.CCTP) { | ||
| _initiateRebalanceCCTP(amount, destinationDomain); | ||
| } else { | ||
| // Unreachable atm, but could become so when more providers are added to enum. | ||
| revert UnsupportedProvider(); | ||
| } | ||
|
|
||
| emit InitiateRebalance(amount, destinationDomain, provider); | ||
| } | ||
|
|
||
| function processRebalance( | ||
| Provider provider, | ||
| bytes calldata extraData | ||
| ) external override { | ||
| if (provider == Provider.CCTP) { | ||
| _processRebalanceCCTP(extraData); | ||
| } else { | ||
| // Unreachable atm, but could become so when more providers are added to enum. | ||
| revert UnsupportedProvider(); | ||
| } | ||
| LIQUIDITY_POOL.deposit(); | ||
|
|
||
| emit ProcessRebalance(provider); | ||
| } | ||
|
|
||
| function _initiateRebalanceCCTP( | ||
| uint256 amount, | ||
| Domain destinationDomain | ||
| ) internal { | ||
| SafeERC20.forceApprove(COLLATERAL, address(CCTP_TOKEN_MESSENGER), amount); | ||
| CCTP_TOKEN_MESSENGER.depositForBurnWithCaller( | ||
| amount, | ||
| domainCCTP(destinationDomain), | ||
| _addressToBytes32(address(LIQUIDITY_POOL)), | ||
| address(COLLATERAL), | ||
| _addressToBytes32(address(this)) | ||
| ); | ||
| } | ||
|
|
||
| function _processRebalanceCCTP( | ||
| bytes calldata extraData | ||
| ) internal { | ||
| (bytes memory message, bytes memory attestation) = abi.decode(extraData, (bytes, bytes)); | ||
| bool success = CCTP_MESSAGE_TRANSMITTER.receiveMessage(message, attestation); | ||
| require(success, ProcessFailed()); | ||
| } | ||
|
|
||
| function domainCCTP(Domain destinationDomain) public pure virtual returns (uint32) { | ||
| if (false) { | ||
| // Intentional unreachable block for better code style. | ||
| return type(uint32).max; | ||
| } else if (destinationDomain == Domain.ETHEREUM) { | ||
| return 0; | ||
| } else if (destinationDomain == Domain.AVALANCHE) { | ||
| return 1; | ||
| } else if (destinationDomain == Domain.OP_MAINNET) { | ||
| return 2; | ||
| } else if (destinationDomain == Domain.ARBITRUM_ONE) { | ||
| return 3; | ||
| } else if (destinationDomain == Domain.BASE) { | ||
| return 6; | ||
| } else if (destinationDomain == Domain.POLYGON_MAINNET) { | ||
| return 7; | ||
| } else { | ||
| revert UnsupportedDomain(); | ||
| } | ||
| } | ||
|
|
||
| function _addressToBytes32(address addr) internal pure returns (bytes32) { | ||
| return bytes32(uint256(uint160(addr))); | ||
| } | ||
|
|
||
| function _getStorage() private pure returns (RebalancerStorage storage $) { | ||
| assembly { | ||
| $.slot := StorageLocation | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
| pragma solidity 0.8.28; | ||
|
|
||
| interface ICCTPTokenMessenger { | ||
| function depositForBurnWithCaller( | ||
| uint256 amount, | ||
| uint32 destinationDomain, | ||
| bytes32 mintRecipient, | ||
| address burnToken, | ||
| bytes32 destinationCaller | ||
| ) external returns (uint64 nonce); | ||
| } | ||
|
|
||
| interface ICCTPMessageTransmitter { | ||
| function receiveMessage(bytes calldata message, bytes calldata signature) | ||
| external | ||
| returns (bool success); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,12 @@ | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
| pragma solidity 0.8.28; | ||
|
|
||
| import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; | ||
|
|
||
| interface ILiquidityPool { | ||
| function deposit() external; | ||
|
|
||
| function withdraw(address to, uint256 amount) external; | ||
|
|
||
| function COLLATERAL() external returns (IERC20); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
| pragma solidity 0.8.28; | ||
|
|
||
| interface IRebalancer { | ||
| // Note, only extend enums at the end to maintain backward compatibility. | ||
| enum Domain { | ||
| ETHEREUM, | ||
| AVALANCHE, | ||
| OP_MAINNET, | ||
| ARBITRUM_ONE, | ||
| BASE, | ||
| POLYGON_MAINNET, | ||
| ETHEREUM_SEPOLIA, | ||
| AVALANCHE_FUJI, | ||
| OP_SEPOLIA, | ||
| ARBITRUM_SEPOLIA, | ||
| BASE_SEPOLIA, | ||
| POLYGON_AMOY | ||
| } | ||
|
|
||
| enum Provider { | ||
| CCTP | ||
| } | ||
|
|
||
| function initiateRebalance( | ||
| uint256 amount, | ||
| Domain destinationDomain, | ||
| Provider provider, | ||
| bytes calldata extraData | ||
| ) external; | ||
|
|
||
| function processRebalance( | ||
| Provider provider, | ||
| bytes calldata extraData | ||
| ) external; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
| pragma solidity 0.8.28; | ||
|
|
||
| import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; | ||
| import {ICCTPTokenMessenger, ICCTPMessageTransmitter} from "../interfaces/ICCTP.sol"; | ||
|
|
||
| interface IBurnable { | ||
| function burn(uint256 value) external; | ||
| } | ||
|
|
||
| interface IMintable { | ||
| function mint(address to, uint256 value) external; | ||
| } | ||
|
|
||
| contract TestCCTPTokenMessenger is ICCTPTokenMessenger { | ||
| function depositForBurnWithCaller( | ||
| uint256 amount, | ||
| uint32 /*destinationDomain*/, | ||
| bytes32 /*mintRecipient*/, | ||
| address burnToken, | ||
| bytes32 /*destinationCaller*/ | ||
| ) external override returns (uint64 nonce) { | ||
| SafeERC20.safeTransferFrom(IERC20(burnToken), msg.sender, address(this), amount); | ||
| IBurnable(burnToken).burn(amount); | ||
| return 1; | ||
| } | ||
| } | ||
|
|
||
| contract TestCCTPMessageTransmitter is ICCTPMessageTransmitter { | ||
| function receiveMessage(bytes calldata message, bytes calldata signature) | ||
| external override returns (bool) | ||
| { | ||
| (address token, address to, uint256 amount) = abi.decode(message, (address, address, uint256)); | ||
| (bool isValid, bool success) = abi.decode(signature, (bool, bool)); | ||
| // solhint-disable-next-line | ||
| require(isValid); | ||
| IMintable(token).mint(to, amount); | ||
| return success; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
| pragma solidity 0.8.28; | ||
|
|
||
| import {Rebalancer} from "../Rebalancer.sol"; | ||
|
|
||
| contract TestRebalancer is Rebalancer { | ||
| constructor( | ||
| address liquidityPool, | ||
| address cctpTokenMessenger, | ||
| address cctpMessageTransmitter | ||
| ) Rebalancer(liquidityPool, cctpTokenMessenger, cctpMessageTransmitter) {} | ||
|
|
||
| function domainCCTP(Domain destinationDomain) public pure override returns (uint32) { | ||
| if (false) { | ||
| // Intentional unreachable block for better code style. | ||
| return type(uint32).max; | ||
| } else if (destinationDomain == Domain.ETHEREUM_SEPOLIA) { | ||
| return 0; | ||
| } else if (destinationDomain == Domain.AVALANCHE_FUJI) { | ||
| return 1; | ||
| } else if (destinationDomain == Domain.OP_SEPOLIA) { | ||
| return 2; | ||
| } else if (destinationDomain == Domain.ARBITRUM_SEPOLIA) { | ||
| return 3; | ||
| } else if (destinationDomain == Domain.BASE_SEPOLIA) { | ||
| return 6; | ||
| } else if (destinationDomain == Domain.POLYGON_AMOY) { | ||
| return 7; | ||
| } else { | ||
| revert UnsupportedDomain(); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what would be the process of adding another provider like across? Like should we move provider specific logic outside of rebalancer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is either an upgrade, or connecting another contract. Both actions require the same privileges, but upgrading keeps the logic more gas efficient.