Skip to content
Open
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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ GNOSIS_CHAIN_RPC=https://public-gno-mainnet.fastnode.io
VIRTUAL_STABLE_RPC=
STABLE_RPC=https://stable-mainnet.rpc.sentio.xyz
TEMPO_RPC=https://rpc.mainnet.tempo.xyz
TRON_RPC=https://tron-rpc.publicnode.com/jsonrpc

# Etherscan now uses a single API key for all chains.
ETHERSCAN_API_KEY=
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ jobs:
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Add Tron Solc
run: mkdir -p ~/.tron/solc/ && cp -n ./bin/soljson_v0.8.26.js ~/.tron/solc/
- name: Compile contracts
run: npm run compile
run: npm run compile-all
- name: Hardhat Tests
run: FORK_BLOCK_NUMBER=49605395 npm run test
- name: Hardhat Fork Tests Ethereum
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,11 @@ crytic-export

CLAUDE.md
.claude/

/artifacts-tron
/cache-tron

/local-temp

/deployments/localtron
/deployments/localhost
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,25 @@ According to CCTP V1 docs, attestation could be produced 9-19 minutes after the
9. Click Create Batch.
10. Click Simulate.
11. Click Send Batch.

# Tron

Tron related modules only work with Node version 20. Latest published compiler is 0.8.24, but tronbox has a 0.8.26 available so we include it in the repo here.

```
nvm use 20

mkdir -p ~/.tron/solc/

cp -n ./bin/soljson_v0.8.26.js ~/.tron/solc/
```

To test local deployments on Tron you need to run a local Tron runtime env.

```
docker pull tronbox/tre

./tron.node.sh

npm run deploy-localtron
```
111 changes: 111 additions & 0 deletions bin/soljson_v0.8.26.js

Large diffs are not rendered by default.

54 changes: 48 additions & 6 deletions contracts/Rebalancer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,26 @@ contract Rebalancer is
);
event ProcessRebalance(uint256 amount, address destinationPool, Provider provider);
event SetRoute(address destinationPool, Domain destinationDomain, Provider provider, bool isAllowed);
event SetThisAddress(Domain domain, bytes32 thisAddress);

error ZeroAmount();
error RouteDenied();
error InvalidRoute();
error UnsupportedProvider();
error InvalidPoolAssets();
error InvalidReceivedToken();
error DestinationDomainNotSupported();

/// @custom:storage-location erc7201:sprinter.storage.Rebalancer
struct RebalancerStorage {
mapping(address pool => BitMaps.BitMap) allowedRoutes;
EnumerableSet.AddressSet knownPools;
mapping(Domain domain => bytes32) thisAddress;
}

struct ThisAddresses {
Domain domain;
bytes32 thisAddress;
}

bytes32 private constant STORAGE_LOCATION = 0x81fbb040176d3bdbf3707b380997ee0038798f9e3ad0bae77fff3621ef225c00;
Expand Down Expand Up @@ -102,11 +110,13 @@ contract Rebalancer is
address rebalancer,
address[] calldata pools,
Domain[] calldata domains,
Provider[] calldata providers
Provider[] calldata providers,
ThisAddresses[] calldata thisAddresses
) external initializer() {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(REBALANCER_ROLE, rebalancer);
_setRoute(pools, domains, providers, true);
_setThisAddresses(thisAddresses);
}

function setRoute(
Expand All @@ -118,6 +128,12 @@ contract Rebalancer is
_setRoute(pools, domains, providers, isAllowed);
}

function setThisAddresses(
ThisAddresses[] calldata thisAddresses
) external onlyRole(DEFAULT_ADMIN_ROLE) {
_setThisAddresses(thisAddresses);
}

function _setRoute(
address[] calldata pools,
Domain[] calldata domains,
Expand Down Expand Up @@ -146,6 +162,24 @@ contract Rebalancer is
}
}

function _setThisAddresses(ThisAddresses[] calldata thisAddresses) internal {
RebalancerStorage storage $ = _getStorage();
for (uint256 i = 0; i < thisAddresses.length; ++i) {
ThisAddresses memory thisAddress = thisAddresses[i];
require(thisAddress.domain != DOMAIN, UnsupportedDomain());
$.thisAddress[thisAddress.domain] = thisAddress.thisAddress;
emit SetThisAddress(thisAddress.domain, thisAddress.thisAddress);
}
}

// Address where Rebalancer is deployed on the destination domain.
function getThisAddress(Domain domain) public view returns (bytes32) {
if (domain == DOMAIN) {
return _addressToBytes32(address(this));
}
return _getStorage().thisAddress[domain];
}

function getAllRoutes()
external view returns (address[] memory pools, Domain[] memory domains, Provider[] memory providers) {
RebalancerStorage storage $ = _getStorage();
Expand Down Expand Up @@ -194,6 +228,8 @@ contract Rebalancer is
Provider provider,
bytes calldata extraData
) external payable override onlyRole(REBALANCER_ROLE) {
bytes32 destinationThisAddress = getThisAddress(destinationDomain);
require(destinationThisAddress != bytes32(0), DestinationDomainNotSupported());
require(amount > 0, ZeroAmount());
require(isRouteAllowed(sourcePool, DOMAIN, Provider.LOCAL), RouteDenied());
require(isRouteAllowed(destinationPool, destinationDomain, provider), RouteDenied());
Expand All @@ -210,18 +246,24 @@ contract Rebalancer is
_processRebalanceLOCAL(amount, destinationPool);
} else
if (provider == Provider.CCTP_V2) {
initiateTransferCCTPV2(ASSETS, amount, destinationPool, destinationDomain);
initiateTransferCCTPV2(ASSETS, amount, destinationPool, destinationDomain, destinationThisAddress);
} else
if (provider == Provider.GNOSIS_OMNIBRIDGE) {
// We need to receive the tokens on the rebalancer contract
// so that we can later call processRebalance(destinationPool, ..., 0x), to make sure we call deposit()
// on the destination pool and swap tokens if needed.
destinationPool = address(this);
initiateTransferGnosisOmnibridge(ASSETS, amount, destinationPool, destinationDomain, DOMAIN);
destinationPool = _bytes32ToAddress(destinationThisAddress);
initiateTransferGnosisOmnibridge(
ASSETS,
amount,
destinationPool,
destinationDomain,
DOMAIN,
destinationThisAddress
);
} else
if (provider == Provider.USDT0) {
destinationPool = address(this);
initiateTransferUSDT0(ASSETS, amount, destinationPool, destinationDomain, extraData, _msgSender());
initiateTransferUSDT0(ASSETS, amount, destinationThisAddress, destinationDomain, extraData, _msgSender());
} else {
revert UnsupportedProvider();
}
Expand Down
70 changes: 62 additions & 8 deletions contracts/Repayer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ contract Repayer is
mapping(address inputToken =>
mapping(bytes32 outputToken => InputOutputTokenData)) inputOutputTokens;
mapping(address pool => IERC20 token) poolOnlySupportsToken;
mapping(Domain domain => bytes32) thisAddress;
}

bytes32 private constant STORAGE_LOCATION = 0xa6615d19cc0b2a17ee46271ca76cd3f303efb9bf682e7eb5c4e7290e895cde00;
Expand All @@ -81,11 +82,13 @@ contract Repayer is
int8 localDecimalsGreaterBy,
bool isAllowed
);
event SetThisAddress(Domain domain, bytes32 thisAddress);

error ZeroAmount();
error RouteDenied();
error UnsupportedProvider();
error InvalidPoolAssets();
error DestinationDomainNotSupported();

struct DestinationToken {
Domain destinationDomain;
Expand All @@ -98,6 +101,11 @@ contract Repayer is
DestinationToken[] destinationTokens;
}

struct ThisAddresses {
Domain domain;
bytes32 thisAddress;
}

constructor(
Domain localDomain,
IERC20 usdc,
Expand Down Expand Up @@ -158,13 +166,15 @@ contract Repayer is
Domain[] calldata domains,
Provider[] calldata providers,
IERC20[] calldata poolOnlySupportsToken,
InputOutputToken[] calldata inputOutputTokens
InputOutputToken[] calldata inputOutputTokens,
ThisAddresses[] calldata thisAddresses
) external initializer() {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(REPAYER_ROLE, repayer);
_grantRole(SET_TOKENS_ROLE, setTokens);
_setRoute(pools, domains, providers, poolOnlySupportsToken, true);
_setInputOutputTokens(inputOutputTokens, true);
_setThisAddresses(thisAddresses);
}

function setRoute(
Expand All @@ -184,6 +194,12 @@ contract Repayer is
_setInputOutputTokens(inputOutputTokens, isAllowed);
}

function setThisAddresses(
ThisAddresses[] calldata thisAddresses
) external onlyRole(DEFAULT_ADMIN_ROLE) {
_setThisAddresses(thisAddresses);
}

/// @notice If the selected provider requires native currency payment to cover fees,
/// then caller has to include it in the transaction. It is then the responsibility
/// of the Adapter to forward the payment and return any change back to the caller.
Expand All @@ -196,6 +212,8 @@ contract Repayer is
Provider provider,
bytes calldata extraData
) external payable override onlyRole(REPAYER_ROLE) {
bytes32 destinationThisAddress = getThisAddress(destinationDomain);
require(destinationThisAddress != bytes32(0), DestinationDomainNotSupported());
require(amount > 0, ZeroAmount());
if (token == WRAPPED_NATIVE_TOKEN) {
uint256 thisBalance = address(this).balance - msg.value;
Expand All @@ -221,7 +239,13 @@ contract Repayer is
_processRepayLOCAL(token, amount, destinationPool);
} else
if (provider == Provider.CCTP_V2) {
initiateTransferCCTPV2(token, amount, destinationPool, destinationDomain);
initiateTransferCCTPV2(
token,
amount,
destinationPool,
destinationDomain,
destinationThisAddress
);
} else
if (provider == Provider.ACROSS) {
initiateTransferAcross(
Expand Down Expand Up @@ -261,10 +285,24 @@ contract Repayer is
if (provider == Provider.GNOSIS_OMNIBRIDGE) {
// When bridging USDC to Gnosis, we must bridge to self to swap USDCxDAI on Gnosis through process().
// It is the Repayment Service responsibility to specify Repayer itself as destination explicitly.
initiateTransferGnosisOmnibridge(token, amount, destinationPool, destinationDomain, DOMAIN);
initiateTransferGnosisOmnibridge(
token,
amount,
destinationPool,
destinationDomain,
DOMAIN,
destinationThisAddress
);
} else
if (provider == Provider.USDT0) {
initiateTransferUSDT0(token, amount, destinationPool, destinationDomain, extraData, _msgSender());
initiateTransferUSDT0(
token,
amount,
_addressToBytes32(destinationPool),
destinationDomain,
extraData,
_msgSender()
);
} else
if (provider == Provider.POLYGON_POS_BRIDGE) {
// When bridging from Polygon, the PoS exit releases the tokens to the burner, so they
Expand Down Expand Up @@ -387,13 +425,31 @@ contract Repayer is
}
}

function _setThisAddresses(ThisAddresses[] calldata thisAddresses) internal {
RepayerStorage storage $ = _getStorage();
for (uint256 i = 0; i < thisAddresses.length; ++i) {
ThisAddresses memory thisAddress = thisAddresses[i];
require(thisAddress.domain != DOMAIN, UnsupportedDomain());
$.thisAddress[thisAddress.domain] = thisAddress.thisAddress;
emit SetThisAddress(thisAddress.domain, thisAddress.thisAddress);
}
}

// Address where Repayer is deployed on the destination domain.
function getThisAddress(Domain domain) public view returns (bytes32) {
if (domain == DOMAIN) {
return _addressToBytes32(address(this));
}
return _getStorage().thisAddress[domain];
}

function getAllRoutes()
external view returns (
address[] memory pools,
Domain[] memory domains,
Provider[] memory providers,
IERC20[] memory poolOnlySupportsToken
)
)
{
RepayerStorage storage $ = _getStorage();
uint256 totalPools = $.knownPools.length();
Expand Down Expand Up @@ -433,9 +489,7 @@ contract Repayer is
}

function isRouteAllowed(address pool, Domain domain, Provider provider) public view returns (bool) {
// As long as we deploy the Repayer contract on the same address on every supported domain this will work.
// If we are to ever deploy it to new address, we will need to change this logic.
if (pool == address(this)) {
if (_addressToBytes32(pool) == getThisAddress(domain)) {
return true;
}
return _getStorage().allowedRoutes[pool].get(_toIndex(domain, provider));
Expand Down
3 changes: 2 additions & 1 deletion contracts/interfaces/IRoute.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ interface IRoute {
INK,
HYPER_EVM,
TEMPO,
STABLE
STABLE,
TRON
}

enum Provider {
Expand Down
4 changes: 3 additions & 1 deletion contracts/testing/RebalancerUSDT0Stable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,12 @@ contract RebalancerUSDT0Stable {
IRoute.Provider[] memory providers = new IRoute.Provider[](2);
providers[0] = IRoute.Provider.LOCAL;
providers[1] = IRoute.Provider.USDT0;
Rebalancer.ThisAddresses[] memory thisAddresses = new Rebalancer.ThisAddresses[](1);
thisAddresses[0] = Rebalancer.ThisAddresses(IRoute.Domain.ARBITRUM_ONE, _addressToBytes32(address(this)));

bytes memory initData = abi.encodeCall(
rebalancerImpl.initialize,
(address(this), address(this), pools, domains, providers)
(address(this), address(this), pools, domains, providers, thisAddresses)
);
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(rebalancerImpl), address(this), initData
Expand Down
4 changes: 3 additions & 1 deletion contracts/testing/RebalancerUSDT0Tempo.sol
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,12 @@ contract RebalancerUSDT0Tempo {
IRoute.Provider[] memory providers = new IRoute.Provider[](2);
providers[0] = IRoute.Provider.LOCAL;
providers[1] = IRoute.Provider.USDT0;
Rebalancer.ThisAddresses[] memory thisAddresses = new Rebalancer.ThisAddresses[](1);
thisAddresses[0] = Rebalancer.ThisAddresses(IRoute.Domain.ARBITRUM_ONE, _addressToBytes32(address(this)));

bytes memory initData = abi.encodeCall(
rebalancerImpl.initialize,
(address(this), address(this), pools, domains, providers)
(address(this), address(this), pools, domains, providers, thisAddresses)
);
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
address(rebalancerImpl), address(this), initData
Expand Down
5 changes: 4 additions & 1 deletion contracts/testing/RepayerUSDT0Stable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ contract RepayerUSDT0Stable {
IERC20[] memory onlySupportedTokens = new IERC20[](1);
onlySupportedTokens[0] = IERC20(address(0));
Repayer.InputOutputToken[] memory inputOutputTokens = new Repayer.InputOutputToken[](0);
Repayer.ThisAddresses[] memory thisAddresses = new Repayer.ThisAddresses[](1);
thisAddresses[0] = Repayer.ThisAddresses(IRoute.Domain.ARBITRUM_ONE, _addressToBytes32(address(this)));

bytes memory initData = abi.encodeCall(
repayerImpl.initialize,
Expand All @@ -98,7 +100,8 @@ contract RepayerUSDT0Stable {
domains,
providers,
onlySupportedTokens,
inputOutputTokens
inputOutputTokens,
thisAddresses
)
);
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
Expand Down
Loading