Address
0x4eae949d24ff11be54ddcd95bc4b91200dffae15Current Holdings
$0.00
TXs sent
not counted
First Active
2026-01-14
block 25,525,120
Last Active
245 days ago
block 25,525,120
Funded By
not identified
Net worth historyi
No net-worth snapshots recorded yet
exact matchPulseXStableSwapAdaptersolc 0.8.28+commit.7893614aruntime exact · creation exact
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IPulseXStableSwapPool} from "../interfaces/IPulseXStableSwapPool.sol";
import {Governable} from "../Governable.sol";
import {DexAdapter} from "./DexAdapter.sol";
/**
* @title PulseXStableSwapAdapter
* @notice Adapter for PulseX StableSwap pools
*/
contract PulseXStableSwapAdapter is DexAdapter, Governable {
using SafeERC20 for IERC20;
error PulseXStableSwapAdapter__OutOfBound();
error PulseXStableSwapAdapter__NotRegisteredPool();
error PulseXStableSwapAdapter__PoolAlreadyRegistered();
error PulseXStableSwapAdapter__InvalidPool();
error PulseXStableSwapAdapter__InsufficientOutput();
error PulseXStableSwapAdapter__InvalidPoolAddress();
error PulseXStableSwapAdapter__ZeroAmountIn();
error PulseXStableSwapAdapter__TooManyPools();
error PulseXStableSwapAdapter__TokenNotFound();
struct PoolData {
bool isRegistered;
mapping(address => uint256) tokenIndex;
address[] tokens;
}
/// @dev Maximum number of pools allowed to prevent gas bloat
uint256 public constant MAX_POOLS = 50;
/// @dev Array of PulseX StableSwap Pool addresses
address[] private pools;
/// @dev Mapping from pool address to its data structure
mapping(address => PoolData) private poolData;
/// @dev Events for pool management
event PoolAdded(address indexed pool, address[] tokens);
event PoolRemoved(address indexed pool, uint256 indexed index);
constructor(address governor, bytes32 _description) DexAdapter(_description) Governable(governor) {}
/**
* @dev Registers a new pool in the adapter. This function can only be called by the contract's governor.
* @param pool The address of the PulseX StableSwap pool to be registered.
* @notice The function will read token addresses from the pool and store their indices.
*/
function addPool(address pool) external onlyGovernor {
if (pool == address(0) || pool.code.length == 0) {
revert PulseXStableSwapAdapter__InvalidPoolAddress();
}
if (pools.length >= MAX_POOLS) {
revert PulseXStableSwapAdapter__TooManyPools();
}
PoolData storage _poolData = poolData[pool];
if (_poolData.isRegistered) {
revert PulseXStableSwapAdapter__PoolAlreadyRegistered();
}
IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(pool);
uint256 nCoins = poolContract.N_COINS();
address[] memory tokens = new address[](nCoins);
for (uint256 i = 0; i < nCoins; i++) {
tokens[i] = poolContract.coins(i);
_poolData.tokenIndex[tokens[i]] = i;
}
_poolData.tokens = tokens;
_poolData.isRegistered = true;
pools.push(pool);
emit PoolAdded(pool, tokens);
}
/**
* @dev Removes a pool from the list. This function can only be called by the contract's governor.
* @param poolIndex The index of the pool in the pools array to be removed.
*/
function removePool(uint256 poolIndex) external onlyGovernor {
uint256 poolsLen = pools.length;
if (poolsLen == 0 || poolIndex >= poolsLen) {
revert PulseXStableSwapAdapter__OutOfBound();
}
address poolAddress = pools[poolIndex];
delete poolData[poolAddress];
if (poolIndex < poolsLen - 1) {
pools[poolIndex] = pools[poolsLen - 1];
}
pools.pop();
emit PoolRemoved(poolAddress, poolIndex);
}
/**
* @dev Gets token indices for a swap in a specific pool
* @param pool The pool address
* @param tokenIn Input token address
* @param tokenOut Output token address
* @return indexIn Index of input token
* @return indexOut Index of output token
*/
function _getTokenIndices(address pool, address tokenIn, address tokenOut)
internal
view
returns (uint256 indexIn, uint256 indexOut)
{
PoolData storage _poolData = poolData[pool];
if (!_poolData.isRegistered) {
revert PulseXStableSwapAdapter__NotRegisteredPool();
}
bool tokenInFound = false;
bool tokenOutFound = false;
for (uint256 i = 0; i < _poolData.tokens.length; i++) {
if (_poolData.tokens[i] == tokenIn) {
indexIn = i;
tokenInFound = true;
}
if (_poolData.tokens[i] == tokenOut) {
indexOut = i;
tokenOutFound = true;
}
if (tokenInFound && tokenOutFound) break;
}
if (!tokenInFound || !tokenOutFound) {
revert PulseXStableSwapAdapter__TokenNotFound();
}
}
/// @inheritdoc DexAdapter
function _swapIn(
address tokenIn,
address tokenOut,
address to,
uint256 amountIn,
uint256 amountOutMin,
bytes memory extraArgs
) internal override returns (uint256 amountOut) {
if (amountIn == 0) revert PulseXStableSwapAdapter__ZeroAmountIn();
address pool = abi.decode(extraArgs, (address));
PoolData storage _poolData = poolData[pool];
if (!_poolData.isRegistered) {
revert PulseXStableSwapAdapter__NotRegisteredPool();
}
(uint256 indexIn, uint256 indexOut) = _getTokenIndices(pool, tokenIn, tokenOut);
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
IERC20(tokenIn).forceApprove(pool, amountIn);
IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(pool);
amountOut = poolContract.get_dy(indexIn, indexOut, amountIn);
if (amountOut < amountOutMin) {
revert PulseXStableSwapAdapter__InsufficientOutput();
}
uint256 balanceBefore = IERC20(tokenOut).balanceOf(address(this));
poolContract.exchange(indexIn, indexOut, amountIn, amountOutMin);
uint256 balanceAfter = IERC20(tokenOut).balanceOf(address(this));
amountOut = balanceAfter - balanceBefore;
if (amountOut < amountOutMin) {
revert PulseXStableSwapAdapter__InsufficientOutput();
}
IERC20(tokenOut).safeTransfer(to, amountOut);
}
/// @inheritdoc DexAdapter
function _swapOut(
address tokenIn,
address tokenOut,
address to,
uint256 amountOut,
uint256 amountInMax,
bytes memory extraArgs
) internal override returns (uint256 amountIn) {
address pool = abi.decode(extraArgs, (address));
PoolData storage _poolData = poolData[pool];
if (!_poolData.isRegistered) {
revert PulseXStableSwapAdapter__NotRegisteredPool();
}
(uint256 indexIn, uint256 indexOut) = _getTokenIndices(pool, tokenIn, tokenOut);
IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(pool);
amountIn = amountInMax;
if (amountIn == 0) {
revert PulseXStableSwapAdapter__ZeroAmountIn();
}
IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
IERC20(tokenIn).forceApprove(pool, amountIn);
uint256 balanceBefore = IERC20(tokenOut).balanceOf(address(this));
poolContract.exchange(indexIn, indexOut, amountIn, 0);
uint256 balanceAfter = IERC20(tokenOut).balanceOf(address(this));
uint256 actualAmountOut = balanceAfter - balanceBefore;
if (actualAmountOut < amountOut) {
revert PulseXStableSwapAdapter__InsufficientOutput();
}
IERC20(tokenOut).safeTransfer(to, actualAmountOut);
}
/// @inheritdoc DexAdapter
function _getMaxOutput(address tokenIn, address tokenOut, uint256 amountIn)
internal
view
override
returns (uint256 maxOutput, bytes memory extraArgs)
{
address targetPool;
uint256 poolsLen = pools.length;
for (uint256 i; i < poolsLen;) {
targetPool = pools[i];
PoolData storage _poolData = poolData[targetPool];
if (_poolData.isRegistered) {
bool tokenInFound = false;
bool tokenOutFound = false;
for (uint256 j = 0; j < _poolData.tokens.length; j++) {
if (_poolData.tokens[j] == tokenIn) tokenInFound = true;
if (_poolData.tokens[j] == tokenOut) tokenOutFound = true;
}
if (tokenInFound && tokenOutFound) {
(uint256 indexIn, uint256 indexOut) = _getTokenIndices(targetPool, tokenIn, tokenOut);
IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(targetPool);
uint256 dy = poolContract.get_dy(indexIn, indexOut, amountIn);
if (dy > maxOutput) {
maxOutput = dy;
extraArgs = abi.encode(targetPool);
}
}
}
unchecked {
++i;
}
}
}
/// @inheritdoc DexAdapter
function _getMinInput(address tokenIn, address tokenOut, uint256 amountOut)
internal
view
override
returns (uint256 minInput, bytes memory extraArgs)
{
address targetPool;
uint256 poolsLen = pools.length;
for (uint256 i; i < poolsLen;) {
targetPool = pools[i];
PoolData storage _poolData = poolData[targetPool];
if (_poolData.isRegistered) {
bool tokenInFound = false;
bool tokenOutFound = false;
for (uint256 j = 0; j < _poolData.tokens.length; j++) {
if (_poolData.tokens[j] == tokenIn) tokenInFound = true;
if (_poolData.tokens[j] == tokenOut) tokenOutFound = true;
}
if (tokenInFound && tokenOutFound) {
(uint256 indexIn, uint256 indexOut) = _getTokenIndices(targetPool, tokenIn, tokenOut);
IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(targetPool);
uint256 dx = _findInputForOutput(poolContract, indexIn, indexOut, amountOut);
if (dx != 0 && (minInput == 0 || dx < minInput)) {
minInput = dx;
extraArgs = abi.encode(targetPool);
}
}
}
unchecked {
++i;
}
}
}
/// @inheritdoc DexAdapter
function _getOutputFromArgs(address tokenIn, address tokenOut, uint256 amountIn, bytes memory extraArgs)
internal
view
override
returns (uint256 amountOut)
{
address pool = abi.decode(extraArgs, (address));
PoolData storage _poolData = poolData[pool];
if (!_poolData.isRegistered) return 0;
bool tokenInFound = false;
bool tokenOutFound = false;
for (uint256 i = 0; i < _poolData.tokens.length; i++) {
if (_poolData.tokens[i] == tokenIn) tokenInFound = true;
if (_poolData.tokens[i] == tokenOut) tokenOutFound = true;
}
if (!tokenInFound || !tokenOutFound) return 0;
(uint256 indexIn, uint256 indexOut) = _getTokenIndices(pool, tokenIn, tokenOut);
IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(pool);
return poolContract.get_dy(indexIn, indexOut, amountIn);
}
/// @inheritdoc DexAdapter
function _getInputFromArgs(address tokenIn, address tokenOut, uint256 amountOut, bytes memory extraArgs)
internal
view
override
returns (uint256 amountIn)
{
address pool = abi.decode(extraArgs, (address));
PoolData storage _poolData = poolData[pool];
if (!_poolData.isRegistered) return 0;
bool tokenInFound = false;
bool tokenOutFound = false;
for (uint256 i = 0; i < _poolData.tokens.length; i++) {
if (_poolData.tokens[i] == tokenIn) tokenInFound = true;
if (_poolData.tokens[i] == tokenOut) tokenOutFound = true;
}
if (!tokenInFound || !tokenOutFound) return 0;
(uint256 indexIn, uint256 indexOut) = _getTokenIndices(pool, tokenIn, tokenOut);
IPulseXStableSwapPool poolContract = IPulseXStableSwapPool(pool);
return _findInputForOutput(poolContract, indexIn, indexOut, amountOut);
}
/// @inheritdoc DexAdapter
function _getAdapterArgs(address tokenIn, address tokenOut)
internal
view
override
returns (bytes[] memory extraArgs)
{
uint256 poolsLen = pools.length;
uint256 validPoolsCount;
address pool;
for (uint256 i; i < poolsLen;) {
pool = pools[i];
PoolData storage _poolData = poolData[pool];
if (_poolData.isRegistered) {
bool tokenInFound = false;
bool tokenOutFound = false;
for (uint256 k = 0; k < _poolData.tokens.length; k++) {
if (_poolData.tokens[k] == tokenIn) tokenInFound = true;
if (_poolData.tokens[k] == tokenOut) tokenOutFound = true;
}
if (tokenInFound && tokenOutFound) {
unchecked {
++validPoolsCount;
}
}
}
unchecked {
++i;
}
}
extraArgs = new bytes[](validPoolsCount);
uint256 j;
for (uint256 i; i < poolsLen;) {
pool = pools[i];
PoolData storage _poolData = poolData[pool];
if (_poolData.isRegistered) {
bool tokenInFound = false;
bool tokenOutFound = false;
for (uint256 k = 0; k < _poolData.tokens.length; k++) {
if (_poolData.tokens[k] == tokenIn) tokenInFound = true;
if (_poolData.tokens[k] == tokenOut) tokenOutFound = true;
}
if (tokenInFound && tokenOutFound) {
extraArgs[j] = abi.encode(pool);
unchecked {
++j;
}
}
}
unchecked {
++i;
}
}
}
/**
* @dev Retrieves details of a specific pool identified by its index in the 'pools' array.
* @param poolIndex The index of the pool in the 'pools' array.
* @return pool The address of the pool at the specified index.
* @return tokens Array of token addresses in the pool.
* @notice The function will return the address of the pool and its tokens.
* It is important to ensure that the poolIndex is within the bounds of the array to avoid out-of-bounds errors.
*/
function getPoolAtIndex(uint256 poolIndex) external view returns (address pool, address[] memory tokens) {
pool = pools[poolIndex];
PoolData storage _poolData = poolData[pool];
tokens = _poolData.tokens;
}
/// @return poolsLength The number of pools currently registered
function allPoolsLength() external view returns (uint256) {
return pools.length;
}
/**
* @dev Finds the required input amount for a desired output using binary search
* @param poolContract The pool contract interface
* @param indexIn Index of input token
* @param indexOut Index of output token
* @param amountOut Desired output amount
* @return amountIn Required input amount
*/
function _findInputForOutput(
IPulseXStableSwapPool poolContract,
uint256 indexIn,
uint256 indexOut,
uint256 amountOut
) internal view returns (uint256 amountIn) {
uint256 low = amountOut;
uint256 high = (amountOut * 200) / 100;
uint256 maxIterations = 100;
for (uint256 i = 0; i < 20; i++) {
uint256 testOutput = poolContract.get_dy(indexIn, indexOut, high);
if (testOutput >= amountOut) {
break;
}
high = (high * 150) / 100;
if (i == 19) {
return 0;
}
}
for (uint256 i = 0; i < maxIterations; i++) {
amountIn = (low + high) / 2;
if (amountIn == low || amountIn == high) {
break;
}
uint256 testOutput = poolContract.get_dy(indexIn, indexOut, amountIn);
if (testOutput >= amountOut) {
high = amountIn;
} else {
low = amountIn;
}
}
amountIn = high;
uint256 finalOutput = poolContract.get_dy(indexIn, indexOut, amountIn);
if (finalOutput < amountOut) {
return 0;
}
}
/**
* @dev Gets the token index for a specific token in a pool
* @param pool The pool address
* @param token The token address
* @return index The index of the token in the pool
*/
function getTokenIndex(address pool, address token) external view returns (uint256 index) {
PoolData storage _poolData = poolData[pool];
if (!_poolData.isRegistered) {
revert PulseXStableSwapAdapter__NotRegisteredPool();
}
index = _poolData.tokenIndex[token];
bool tokenFound = false;
for (uint256 i = 0; i < _poolData.tokens.length; i++) {
if (_poolData.tokens[i] == token) {
tokenFound = true;
break;
}
}
if (!tokenFound) {
revert PulseXStableSwapAdapter__TokenNotFound();
}
}
}