Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Contracts admin.
ADMIN=
# USDC token address. Leave empty to deploy a test one instead.
USDC=
# Rebalance caller.
REBALANCE_CALLER=
# Deposits to Liquidity Hub are only allowed till this limit is reached.
ASSETS_LIMIT=
# Liquidity mining tiers. Multiplier will be divided by 100. So 175 will result in 1.75x.
Expand All @@ -12,5 +12,5 @@ TIER_2_DAYS=180
TIER_2_MULTIPLIER=150
TIER_3_DAYS=360
TIER_3_MULTIPLIER=200
BASETEST_PRIVATE_KEY=
BASE_SEPOLIA_PRIVATE_KEY=
VERIFY=false
3 changes: 2 additions & 1 deletion .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"rules": {
"max-line-length": ["off", 120],
"func-visibility": ["warn", {"ignoreConstructors": true}],
"func-name-mixedcase": ["off"]
"func-name-mixedcase": ["off"],
"one-contract-per-file": ["off"]
}
}
185 changes: 185 additions & 0 deletions contracts/Rebalancer.sol
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 {
Copy link
Member

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?

Copy link
Collaborator Author

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.

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
}
}
}
18 changes: 18 additions & 0 deletions contracts/interfaces/ICCTP.sol
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);
}
4 changes: 4 additions & 0 deletions contracts/interfaces/ILiquidityPool.sol
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);
}
36 changes: 36 additions & 0 deletions contracts/interfaces/IRebalancer.sol
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;
}
40 changes: 40 additions & 0 deletions contracts/testing/TestCCTP.sol
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;
}
}
9 changes: 5 additions & 4 deletions contracts/testing/TestLiquidityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ import {ILiquidityPool} from "../interfaces/ILiquidityPool.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";

contract TestLiquidityPool is ILiquidityPool, AccessControl {
IERC20 private immutable ASSET;
IERC20 public immutable COLLATERAL;

event Deposit();

constructor(IERC20 asset) {
ASSET = asset;
constructor(IERC20 collateral) {
COLLATERAL = collateral;
_grantRole(DEFAULT_ADMIN_ROLE, _msgSender());
}

function deposit() external override {
emit Deposit();
}

function withdraw(address to, uint256 amount) external override onlyRole(DEFAULT_ADMIN_ROLE) {
SafeERC20.safeTransfer(ASSET, to, amount);
SafeERC20.safeTransfer(COLLATERAL, to, amount);
}
}
33 changes: 33 additions & 0 deletions contracts/testing/TestRebalancer.sol
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();
}
}
}
8 changes: 8 additions & 0 deletions contracts/testing/TestUSDC.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,12 @@ contract TestUSDC is ERC20, ERC20Permit {
function decimals() public pure override returns (uint8) {
return 6;
}

function burn(uint256 amount) public {
_burn(msg.sender, amount);
}

function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
Loading