Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2f0d90c
feat: polyfon pos bridge adapter
mpetrunic Aug 7, 2026
f847f79
Cleanup, fix tests and CI
lastperson Aug 10, 2026
9e3528c
Merge branch 'main' into feat/polygon-pos-bridge
lastperson Aug 10, 2026
e21d5de
Update coverage
lastperson Aug 10, 2026
70a87ba
Add Stable and Tempo -> Arbitrum routes
lastperson Aug 14, 2026
f267176
Add Tron support
lastperson Aug 14, 2026
ea09465
Add Tron Node version requirement to readme
lastperson Aug 14, 2026
0a8891c
Cleanup types
lastperson Aug 14, 2026
06b3dd0
Merge branch 'feat/polygon-pos-bridge' into feat/tron
lastperson Aug 17, 2026
09b5518
Merge branch 'main' into feat/tron
lastperson Aug 17, 2026
8bf9f25
Add tron solc binary
lastperson Aug 18, 2026
bd603a4
Fix lint
lastperson Aug 18, 2026
9380749
Further refine Tron deployment script
lastperson Aug 18, 2026
416e1ab
Add stage deployment command
lastperson Aug 18, 2026
7e84d28
Add Tron Stage deployment artifacts
lastperson Aug 19, 2026
5343026
Modify Rebalancer and Repayer
lastperson Aug 20, 2026
835d2fc
Update scripts for Repayer/Rebalancer changes
lastperson Aug 21, 2026
4e9585a
Add Tron rebalancer/repayer upgrade scripts
lastperson Aug 21, 2026
1e02de4
Add Tron scripts resources calculator
lastperson Aug 21, 2026
16d4362
Fix Polygon tests
lastperson Aug 21, 2026
6c2f13b
Update coverage baseline
lastperson Aug 21, 2026
0ec84fd
Add tron update thisaddresses scripts
lastperson Aug 21, 2026
2d26e24
Merge branch 'main' into feat/tron
mpetrun5 Sep 7, 2026
3b67927
Add missing comma
mpetrun5 Sep 7, 2026
b20c8ec
Replace network ordering
mpetrun5 Sep 8, 2026
246a2c8
Lint
mpetrun5 Sep 8, 2026
d3b8f54
Fix tests
mpetrun5 Sep 8, 2026
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
ROBINHOOD_RPC=https://rpc.mainnet.chain.robinhood.com

# Etherscan now uses a single API key for all chains.
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 @@ -25,7 +25,8 @@ interface IRoute {
HYPER_EVM,
TEMPO,
STABLE,
ROBINHOOD
ROBINHOOD,
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
Loading