Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3ad49e3
Add PublicLiquidityPool and ERC4626Adapter, wip
lastperson Oct 28, 2025
d6acef0
Get fill amount from calldata in Public pool
lastperson Oct 30, 2025
28d9f40
Add most of the Public pool tests, wip
lastperson Oct 30, 2025
7845908
Complete Public Liquidity Pool tests
lastperson Nov 3, 2025
dfdfa99
Add ERC4626Adapter tests
lastperson Nov 3, 2025
c83257f
Add totalDeposited getter to public pool
lastperson Nov 5, 2025
930488f
Revert back to FeeConfig in public pool
lastperson Nov 7, 2025
8cedcc7
Revert back to parsing calldata
lastperson Nov 10, 2025
f3624cf
Merge branch 'main' into feat-public-pool
lastperson Nov 10, 2025
fda91a0
Fix typo
lastperson Nov 10, 2025
3bd1896
Apply audit fixes 1
lastperson Nov 14, 2025
7e5c406
Merge branch 'main' into feat-public-pool
lastperson Nov 20, 2025
21e247c
Add public pool deploy scripts (#128)
lastperson Nov 20, 2025
ac40032
Fix chain id in tests
lastperson Nov 20, 2025
8e05179
Add various fixes
lastperson Nov 25, 2025
c96084c
Remove console.log from tests
lastperson Nov 25, 2025
24a0124
Merge branch 'main' of github.com:sprintertech/sprinter-stash-contrac…
lastperson Nov 25, 2025
cb780a6
Merge main and fix tests
lastperson Nov 25, 2025
d399e83
Merge branch 'feat-public-pool' into fix-audit-4
lastperson Nov 26, 2025
a591da5
Make all tests run
lastperson Nov 26, 2025
7fd2f52
Last part of fixes
lastperson Nov 26, 2025
d2ac2c5
Make public pool maxWithdraw and maxRedeem compliant with the EIP4626
lastperson Nov 26, 2025
15b0372
Fix lint
lastperson Nov 26, 2025
8a93608
Improve ERC4626Adapter withdraw profit safety assertion
lastperson Nov 27, 2025
15e29a6
Make pools return 0 balance when paused
lastperson Nov 28, 2025
efc8e9c
Replace Optimism with Superchain bridge, add Base support
lastperson Nov 28, 2025
6f1f78a
Add input output token map to Repayer, wip
lastperson Dec 1, 2025
8adc9b1
Add repayer tokens config parsing to scripts
lastperson Dec 2, 2025
4e143d8
Fix lint
lastperson Dec 2, 2025
6394b9f
Audit fixes 4 (#135)
lastperson Dec 2, 2025
0f98d6b
Merge branch 'feat-public-pool' of github.com:sprintertech/sprinter-s…
lastperson Dec 2, 2025
39c192b
Add new contract version ids
lastperson Dec 2, 2025
b17389d
Cleanup
lastperson Dec 2, 2025
7f6cc91
Merge branch 'main' of github.com:sprintertech/sprinter-stash-contrac…
lastperson Dec 2, 2025
0bbbac2
Merge branch 'main' into feat-various
lastperson Dec 2, 2025
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,12 @@ You can optionally set VERIFY to `true` in order to publish the source code to E

### Hardhat tasks

In order to update onchain rebalancer or repayer routes to reflect what is put into configuration, execute the following tasks:
In order to update onchain rebalancer or repayer configurations to reflect what is put into configuration, execute the following tasks:

```
npm run hardhat -- update-routes-rebalancer --network BASE
npm run hardhat -- update-routes-repayer --network BASE
npm run hardhat -- add-tokens-repayer --network BASE
```

It will produce a list of instructions for the admin multisig.
Expand Down
1 change: 1 addition & 0 deletions contracts/LiquidityPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ contract LiquidityPool is ILiquidityPool, AccessControl, EIP712, ISigner {
}

function balance(IERC20 token) external view override returns (uint256) {
if (paused || borrowPaused) return 0;
if (token == NATIVE_TOKEN) token = WRAPPED_NATIVE_TOKEN;
return _balance(token);
}
Expand Down
93 changes: 83 additions & 10 deletions contracts/Repayer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {CCTPAdapter} from "./utils/CCTPAdapter.sol";
import {AcrossAdapter} from "./utils/AcrossAdapter.sol";
import {StargateAdapter} from "./utils/StargateAdapter.sol";
import {EverclearAdapter} from "./utils/EverclearAdapter.sol";
import {OptimismStandardBridgeAdapter} from "./utils/OptimismStandardBridgeAdapter.sol";
import {SuperchainStandardBridgeAdapter} from "./utils/SuperchainStandardBridgeAdapter.sol";
import {ERC7201Helper} from "./utils/ERC7201Helper.sol";

/// @title Performs repayment to Liquidity Pools on same/different chains.
Expand All @@ -28,7 +28,7 @@ contract Repayer is
AcrossAdapter,
StargateAdapter,
EverclearAdapter,
OptimismStandardBridgeAdapter
SuperchainStandardBridgeAdapter
{
using SafeERC20 for IERC20;
using BitMaps for BitMaps.BitMap;
Expand All @@ -37,13 +37,16 @@ contract Repayer is
Domain immutable public DOMAIN;
IERC20 immutable public ASSETS;
bytes32 constant public REPAYER_ROLE = "REPAYER_ROLE";
bytes32 constant public SET_TOKENS_ROLE = "SET_TOKENS_ROLE";
IWrappedNativeToken immutable public WRAPPED_NATIVE_TOKEN;

/// @custom:storage-location erc7201:sprinter.storage.Repayer
struct RepayerStorage {
mapping(address pool => BitMaps.BitMap) allowedRoutes;
EnumerableSet.AddressSet knownPools;
mapping(address pool => bool) poolSupportsAllTokens;
mapping(address inputToken =>
mapping(bytes32 outputToken => BitMaps.BitMap destinationDomains)) inputOutputTokens;
}

bytes32 private constant STORAGE_LOCATION = 0xa6615d19cc0b2a17ee46271ca76cd3f303efb9bf682e7eb5c4e7290e895cde00;
Expand All @@ -63,6 +66,7 @@ contract Repayer is
Provider provider
);
event ProcessRepay(IERC20 token, uint256 amount, address destinationPool, Provider provider);
event SetInputOutputToken(address inputToken, Domain destinationDomain, bytes32 outputToken, bool isAllowed);

error ZeroAmount();
error InsufficientBalance();
Expand All @@ -71,6 +75,16 @@ contract Repayer is
error UnsupportedProvider();
error InvalidPoolAssets();

struct DestinationToken {
Domain destinationDomain;
bytes32 outputToken;
}

struct InputOutputToken {
address inputToken;
DestinationToken[] destinationTokens;
}

constructor(
Domain localDomain,
IERC20 assets,
Expand All @@ -80,13 +94,14 @@ contract Repayer is
address everclearFeeAdapter,
address wrappedNativeToken,
address stargateTreasurer,
address optimismBridge
address optimismBridge,
address baseBridge
)
CCTPAdapter(cctpTokenMessenger, cctpMessageTransmitter)
AcrossAdapter(acrossSpokePool)
StargateAdapter(stargateTreasurer)
EverclearAdapter(everclearFeeAdapter)
OptimismStandardBridgeAdapter(optimismBridge, wrappedNativeToken)
SuperchainStandardBridgeAdapter(optimismBridge, baseBridge, wrappedNativeToken)
{
ERC7201Helper.validateStorageLocation(
STORAGE_LOCATION,
Expand All @@ -106,14 +121,18 @@ contract Repayer is
function initialize(
address admin,
address repayer,
address setTokens,
address[] calldata pools,
Domain[] calldata domains,
Provider[] calldata providers,
bool[] calldata poolSupportsAllTokens
bool[] calldata poolSupportsAllTokens,
InputOutputToken[] calldata inputOutputTokens
) external initializer() {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(REPAYER_ROLE, repayer);
_grantRole(SET_TOKENS_ROLE, setTokens);
_setRoute(pools, domains, providers, poolSupportsAllTokens, true);
_setInputOutputTokens(inputOutputTokens, true);
}

function setRoute(
Expand All @@ -126,6 +145,13 @@ contract Repayer is
_setRoute(pools, domains, providers, poolSupportsAllTokens, isAllowed);
}

function setInputOutputTokens(
InputOutputToken[] calldata inputOutputTokens,
bool isAllowed
) external onlyRole(SET_TOKENS_ROLE) {
_setInputOutputTokens(inputOutputTokens, isAllowed);
}

/// @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 Down Expand Up @@ -167,17 +193,37 @@ contract Repayer is
initiateTransferCCTP(token, amount, destinationPool, destinationDomain);
} else
if (provider == Provider.ACROSS) {
initiateTransferAcross(token, amount, destinationPool, destinationDomain, extraData);
initiateTransferAcross(
token,
amount,
destinationPool,
destinationDomain,
extraData,
$.inputOutputTokens[address(token)]
);
} else
if (provider == Provider.EVERCLEAR) {
initiateTransferEverclear(token, amount, destinationPool, destinationDomain, extraData);
initiateTransferEverclear(
token,
amount,
destinationPool,
destinationDomain,
extraData,
$.inputOutputTokens[address(token)]
);
} else
if (provider == Provider.STARGATE) {
initiateTransferStargate(token, amount, destinationPool, destinationDomain, extraData, _msgSender());
} else
if (provider == Provider.OPTIMISM_STANDARD_BRIDGE) {
initiateTransferOptimismStandardBridge(
token, amount, destinationPool, destinationDomain, extraData, DOMAIN
if (provider == Provider.SUPERCHAIN_STANDARD_BRIDGE) {
initiateTransferSuperchainStandardBridge(
token,
amount,
destinationPool,
destinationDomain,
extraData,
DOMAIN,
$.inputOutputTokens[address(token)]
);
} else {
// Unreachable atm, but could become so when more providers are added to enum.
Expand Down Expand Up @@ -244,6 +290,25 @@ contract Repayer is
}
}

function _setInputOutputTokens(
InputOutputToken[] calldata inputOutputTokens,
bool isAllowed
) internal {
RepayerStorage storage $ = _getStorage();
for (uint256 i = 0; i < inputOutputTokens.length; ++i) {
address inputToken = inputOutputTokens[i].inputToken;
DestinationToken[] calldata destinationTokens = inputOutputTokens[i].destinationTokens;
for (uint256 j = 0; j < destinationTokens.length; ++j) {
DestinationToken calldata destinationToken = destinationTokens[j];
Domain destinationDomain = destinationToken.destinationDomain;
bytes32 outputToken = destinationToken.outputToken;
require(destinationDomain != DOMAIN, UnsupportedDomain());
$.inputOutputTokens[inputToken][outputToken].setTo(uint256(destinationDomain), isAllowed);
emit SetInputOutputToken(inputToken, destinationDomain, outputToken, isAllowed);
}
}
}

function getAllRoutes()
external view returns (
address[] memory pools,
Expand Down Expand Up @@ -293,6 +358,14 @@ contract Repayer is
return _getStorage().allowedRoutes[pool].get(_toIndex(domain, provider));
}

function isOutputTokenAllowed(
address inputToken,
Domain destinationDomain,
bytes32 outputToken
) public view returns (bool) {
return _getStorage().inputOutputTokens[inputToken][outputToken].get(uint256(destinationDomain));
}

function _getStorage() private pure returns (RepayerStorage storage $) {
assembly {
$.slot := STORAGE_LOCATION
Expand Down
2 changes: 1 addition & 1 deletion contracts/interfaces/IRoute.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface IRoute {
ACROSS,
STARGATE,
EVERCLEAR,
OPTIMISM_STANDARD_BRIDGE
SUPERCHAIN_STANDARD_BRIDGE
}

enum PoolType {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.0;

interface IOptimismStandardBridge {
interface ISuperchainStandardBridge {

/// @notice Emitted when an ERC20 bridge is initiated to the other chain.
/// @param localToken Address of the ERC20 on this chain.
Expand Down
6 changes: 4 additions & 2 deletions contracts/testing/TestRepayer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ contract TestRepayer is Repayer {
address everclearFeeAdapter,
address wrappedNativeToken,
address stargateTreasurer,
address optimismBridge
address optimismBridge,
address baseBridge
) Repayer(
localDomain,
assets,
Expand All @@ -23,7 +24,8 @@ contract TestRepayer is Repayer {
everclearFeeAdapter,
wrappedNativeToken,
stargateTreasurer,
optimismBridge
optimismBridge,
baseBridge
) {}

function domainCCTP(Domain destinationDomain) public pure override returns (uint32) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
pragma solidity 0.8.28;

import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IOptimismStandardBridge} from "../interfaces/IOptimism.sol";
import {ISuperchainStandardBridge} from "../interfaces/ISuperchainStandardBridge.sol";

contract TestOptimismStandardBridge is IOptimismStandardBridge {
error OptimismBridgeWrongRemoteToken();
error OptimismBridgeWrongMinGasLimit();
contract TestSuperchainStandardBridge is ISuperchainStandardBridge {
error SuperchainStandardBridgeWrongRemoteToken();
error SuperchainStandardBridgeWrongMinGasLimit();

function bridgeERC20To(
address _localToken,
Expand All @@ -18,7 +18,7 @@ contract TestOptimismStandardBridge is IOptimismStandardBridge {
) external override {
require(
_localToken != _remoteToken,
OptimismBridgeWrongRemoteToken()
SuperchainStandardBridgeWrongRemoteToken()
); // To simulate revert.
SafeERC20.safeTransferFrom(IERC20(_localToken), msg.sender, address(this), _amount);
emit ERC20BridgeInitiated(
Expand All @@ -34,7 +34,7 @@ contract TestOptimismStandardBridge is IOptimismStandardBridge {
function bridgeETHTo(address _to, uint32 _minGasLimit, bytes calldata _extraData) external payable override {
require(
_minGasLimit > 0,
OptimismBridgeWrongMinGasLimit()
SuperchainStandardBridgeWrongMinGasLimit()
); // To simulate revert.
emit ETHBridgeInitiated(address(this), _to, msg.value, _extraData);
}
Expand Down
7 changes: 6 additions & 1 deletion contracts/utils/AcrossAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity 0.8.28;
import {V3SpokePoolInterface} from ".././interfaces/IAcross.sol";
import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {AdapterHelper} from "./AdapterHelper.sol";
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";

abstract contract AcrossAdapter is AdapterHelper {
using SafeERC20 for IERC20;
Expand All @@ -22,7 +23,8 @@ abstract contract AcrossAdapter is AdapterHelper {
uint256 amount,
address destinationPool,
Domain destinationDomain,
bytes calldata extraData
bytes calldata extraData,
mapping(bytes32 => BitMaps.BitMap) storage outputTokens
) internal notPayable {
require(address(ACROSS_SPOKE_POOL) != address(0), ZeroAddress());
token.forceApprove(address(ACROSS_SPOKE_POOL), amount);
Expand All @@ -38,6 +40,9 @@ abstract contract AcrossAdapter is AdapterHelper {
// then we will need to remove this requirement.
// Until then we leave it here as a protective measure on potential offchain component calculation errors.
require(outputAmount >= (amount * 9980 / 10000), SlippageTooHigh());
if (outputToken != address(0)) {
_validateOutputToken(_addressToBytes32(outputToken), destinationDomain, outputTokens);
}
ACROSS_SPOKE_POOL.depositV3(
address(this),
destinationPool,
Expand Down
12 changes: 12 additions & 0 deletions contracts/utils/AdapterHelper.sol
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.28;

import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
import {IRoute} from ".././interfaces/IRoute.sol";

abstract contract AdapterHelper is IRoute {
using BitMaps for BitMaps.BitMap;

error SlippageTooHigh();
error NotPayable();
error InvalidOutputToken();

modifier notPayable() {
require(msg.value == 0, NotPayable());
Expand Down Expand Up @@ -47,4 +51,12 @@ abstract contract AdapterHelper is IRoute {
function _addressToBytes32(address addr) internal pure returns (bytes32) {
return bytes32(uint256(uint160(addr)));
}

function _validateOutputToken(
bytes32 outputToken,
Domain destinationDomain,
mapping(bytes32 outputToken => BitMaps.BitMap destinationDomains) storage outputTokens
) internal view {
require(outputTokens[outputToken].get(uint256(destinationDomain)), InvalidOutputToken());
}
}
5 changes: 4 additions & 1 deletion contracts/utils/EverclearAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity 0.8.28;
import {IFeeAdapterV2} from ".././interfaces/IEverclear.sol";
import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {AdapterHelper} from "./AdapterHelper.sol";
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";

abstract contract EverclearAdapter is AdapterHelper {
using SafeERC20 for IERC20;
Expand All @@ -22,7 +23,8 @@ abstract contract EverclearAdapter is AdapterHelper {
uint256 amount,
address destinationPool,
Domain destinationDomain,
bytes calldata extraData
bytes calldata extraData,
mapping(bytes32 => BitMaps.BitMap) storage outputTokens
) internal {
require(address(EVERCLEAR_FEE_ADAPTER) != address(0), ZeroAddress());
token.forceApprove(address(EVERCLEAR_FEE_ADAPTER), amount);
Expand All @@ -33,6 +35,7 @@ abstract contract EverclearAdapter is AdapterHelper {
IFeeAdapterV2.FeeParams memory feeParams
) = abi.decode(extraData, (bytes32, uint256, uint48, IFeeAdapterV2.FeeParams));
require(amountOutMin >= (amount * 9980 / 10000), SlippageTooHigh());
_validateOutputToken(outputAsset, destinationDomain, outputTokens);
uint32[] memory destinations = new uint32[](1);
destinations[0] = domainChainId(destinationDomain);
EVERCLEAR_FEE_ADAPTER.newIntent{value: msg.value}(
Expand Down
Loading