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
77 changes: 67 additions & 10 deletions contracts/Processor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {IRoyco} from "./interfaces/IRoyco.sol";
import {IERC7540} from "./interfaces/IERC7540.sol";
import {SubProcessor} from "./SubProcessor.sol";
import {ERC7201Helper} from "./utils/ERC7201Helper.sol";
import {IOracle} from "./interfaces/IOracle.sol";

/// @title Processor for unwinding vault tokens.
/// @author Sprinter
Expand All @@ -18,6 +19,7 @@ contract Processor is AccessControlUpgradeable, MulticallUpgradeable {

IERC20 public immutable TARGET_ASSET;
address public immutable RECEIVER;
IOracle immutable public ORACLE;

uint256 public constant MULTIPLIER = 100_00;

Expand All @@ -33,7 +35,7 @@ contract Processor is AccessControlUpgradeable, MulticallUpgradeable {
bytes32 private constant STORAGE_LOCATION = 0x315f94a0eb28ebc9ade3d51c8bd7ef4c2011752d9ac5c2acc448e4822cfa1f00;

event Forwarded(address caller, IERC20 token);
event Processed(address caller, IERC4626 tokenIn, uint256 sharesIn, uint256 amountOut);
event Processed(address caller, IERC20 tokenIn, uint256 amountIn, uint256 amountOut);
event MaxSlippageSet(uint256 maxSlippage);
Comment on lines 37 to 39
event AdminProcessed(address caller);

Expand All @@ -44,8 +46,9 @@ contract Processor is AccessControlUpgradeable, MulticallUpgradeable {
error InsufficientAssets();
error InvalidSlippage();
error AlreadyInitialized();
error DeadlineExceeded();

constructor(address asset, address receiver) {
constructor(address asset, address receiver, address oracle) {
ERC7201Helper.validateStorageLocation(
STORAGE_LOCATION,
"sprinter.storage.Processor"
Expand All @@ -55,6 +58,7 @@ contract Processor is AccessControlUpgradeable, MulticallUpgradeable {
require(receiver != address(0), ZeroAddress());
TARGET_ASSET = IERC20(asset);
RECEIVER = receiver;
ORACLE = IOracle(oracle);
}

function initialize(
Expand Down Expand Up @@ -91,8 +95,7 @@ contract Processor is AccessControlUpgradeable, MulticallUpgradeable {
}

function forward(IERC20 token) external onlyRole(CALLER_ROLE) {
token.safeTransfer(RECEIVER, token.balanceOf(address(this)));

_finalizeTransfer(token.balanceOf(address(this)));
emit Forwarded(msg.sender, token);
}
Comment on lines 97 to 100

Expand Down Expand Up @@ -129,21 +132,63 @@ contract Processor is AccessControlUpgradeable, MulticallUpgradeable {
uint256 amountOutMin,
SubProcessor.Call[] calldata calls
) external onlyRole(CALLER_ROLE) {
require(sharesIn > 0, ZeroAmount());
require(address(tokenIn) != address(TARGET_ASSET), InvalidTokenIn());
ProcessorStorage storage $ = _getStorage();
uint256 redeemResult = tokenIn.convertToAssets(sharesIn);
require(redeemResult * (MULTIPLIER - uint256($.maxSlippage)) / MULTIPLIER <= amountOutMin, SlippageTooHigh());
IERC20(address(tokenIn)).safeTransfer(address($.subProcessor), sharesIn);
_assertOutputAmount($, redeemResult, amountOutMin);
_process($, tokenIn, sharesIn, amountOutMin, calls);
}

/// @notice Signature is not used at the moment, but might be useful in the future.
function process(
IERC20 tokenIn,
uint256 amountIn,
uint256 amountOutMin,
uint256 deadline,
bytes calldata signature,
SubProcessor.Call[] calldata calls
) external onlyRole(CALLER_ROLE) {
require(block.timestamp <= deadline, DeadlineExceeded());
ProcessorStorage storage $ = _getStorage();
_processSignature(tokenIn, amountIn, amountOutMin, deadline, signature);
_process($, tokenIn, amountIn, amountOutMin, calls);
}

function _process(
ProcessorStorage storage $,
IERC20 tokenIn,
uint256 amountIn,
uint256 amountOutMin,
SubProcessor.Call[] calldata calls
) internal {
require(amountIn > 0, ZeroAmount());
require(tokenIn != TARGET_ASSET, InvalidTokenIn());
tokenIn.safeTransfer(address($.subProcessor), amountIn);
uint256 assets = TARGET_ASSET.balanceOf(address(this));
$.subProcessor.process(calls);
uint256 assetsAfter = TARGET_ASSET.balanceOf(address(this));
require(assetsAfter >= assets, InsufficientAssets());
uint256 amountOut = assetsAfter - assets;
require(amountOut >= amountOutMin, InsufficientAssets());
TARGET_ASSET.safeTransfer(RECEIVER, assetsAfter);
_finalizeTransfer(amountOut);
emit Processed(msg.sender, tokenIn, amountIn, amountOut);
}

emit Processed(msg.sender, tokenIn, sharesIn, amountOut);
function _finalizeTransfer(uint256 amount) internal virtual {
TARGET_ASSET.safeTransfer(RECEIVER, amount);
}

/// @notice Currently consults oracle instead of checking the signature.
function _processSignature(
IERC20 tokenIn,
uint256 amountIn,
uint256 amountOutMin,
uint256,
bytes calldata
) internal view {
ProcessorStorage storage $ = _getStorage();
uint256 valueIn = ORACLE.getAssetValue(_tokenToAssetId(tokenIn), amountIn);
uint256 valueOut = ORACLE.getAssetValue(_tokenToAssetId(TARGET_ASSET), amountOutMin);
_assertOutputAmount($, valueIn, valueOut);
}

function adminProcess(SubProcessor.Call[] calldata calls) external onlyRole(CONFIG_ROLE) {
Expand All @@ -158,6 +203,18 @@ contract Processor is AccessControlUpgradeable, MulticallUpgradeable {
emit MaxSlippageSet(newMaxSlippage);
}

function _tokenToAssetId(address token) internal pure returns (bytes32) {
return bytes32(uint256(uint160(token)));
}

function _tokenToAssetId(IERC20 token) internal pure returns (bytes32) {
return _tokenToAssetId(address(token));
}

function _assertOutputAmount(ProcessorStorage storage $, uint256 valueIn, uint256 valueOut) internal view {
require(valueIn * (MULTIPLIER - uint256($.maxSlippage)) <= valueOut * MULTIPLIER, SlippageTooHigh());
}

function _getStorage() private pure returns (ProcessorStorage storage $) {
assembly {
$.slot := STORAGE_LOCATION
Expand Down
5 changes: 3 additions & 2 deletions contracts/StashDex.sol
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,9 @@ contract StashDex is AccessControlUpgradeable {
RouteConfig memory route = $.routes[tokenIn][tokenOut];
require(route.allowed, RouteNotAllowed());

uint256 valueIn = ORACLE.getAssetValue(_tokenToAssetId(tokenIn), amountIn * 10**12);
uint256 valueOut = ORACLE.getAssetValue(_tokenToAssetId(tokenOut), amountOut * 10**12);
uint256 precision = 10**12;
uint256 valueIn = ORACLE.getAssetValue(_tokenToAssetId(tokenIn), amountIn * precision);
uint256 valueOut = ORACLE.getAssetValue(_tokenToAssetId(tokenOut), amountOut * precision);
require(valueIn * (BPS - route.feeBps) >= valueOut * BPS, InsufficientOutput());

IERC20(tokenIn).safeTransferFrom(_msgSender(), route.processor, amountIn);
Expand Down
20 changes: 20 additions & 0 deletions contracts/StashDexProcessor.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.28;

import {Processor} from "./Processor.sol";
import {IStashDex} from "./interfaces/IStashDex.sol";

/// @title StashDexProcessor — extends Processor to repay StashDex debt after each transfer.
/// @notice RECEIVER must be a StashDex contract. After forwarding TARGET_ASSET to StashDex,
/// repay() is called so StashDex immediately settles its outstanding pool debt.
/// @author Sprinter
contract StashDexProcessor is Processor {
constructor(address asset, address receiver, address oracle) Processor(asset, receiver, oracle) {
require(oracle != address(0), ZeroAddress());
}

function _finalizeTransfer(uint256 amount) internal override {
super._finalizeTransfer(amount);
IStashDex(RECEIVER).repay(address(TARGET_ASSET));
}
}
8 changes: 4 additions & 4 deletions contracts/echidna/EchidnaLiquidityHub.sol
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ contract EchidnaLiquidityHub {
function testWithdraw(uint256 amount) public {
// Preconditions
uint256 assets = hub.totalAssets();
uint256 balanceSharesBeforeDeposit = shares.balanceOf(address(this));
require(balanceSharesBeforeDeposit > 0 || assets > liquidityToken.balanceOf(address(pool)), RequireFailed());
uint256 balanceSharesBeforeDeposit = hub.totalSupply();
require(balanceSharesBeforeDeposit > 0 || assets <= liquidityToken.balanceOf(address(pool)), RequireFailed());
require(amount > 0, RequireFailed());
require(amount <= hub.maxDeposit(address(this)), RequireFailed());
// require(shares.balanceOf(address(this)) >= amount);
Expand Down Expand Up @@ -159,8 +159,8 @@ contract EchidnaLiquidityHub {
function testRedeem(uint256 amount) public {
// Preconditions
uint256 assets = hub.totalAssets();
uint256 balanceSharesBeforeDeposit = shares.balanceOf(address(this));
require(balanceSharesBeforeDeposit > 0 || assets > liquidityToken.balanceOf(address(pool)), RequireFailed());
uint256 balanceSharesBeforeDeposit = hub.totalSupply();
require(balanceSharesBeforeDeposit > 0 || assets <= liquidityToken.balanceOf(address(pool)), RequireFailed());
require(assets > 0, RequireFailed());
require((amount > assets / 10 ** 12) && (amount > 1), RequireFailed());
require(amount <= hub.maxDeposit(address(this)), RequireFailed());
Expand Down
6 changes: 6 additions & 0 deletions contracts/interfaces/IStashDex.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.28;

interface IStashDex {
function repay(address token) external;
}
4 changes: 2 additions & 2 deletions coverage-baseline.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"lines": "99.71",
"functions": "99.68",
"branches": "93.18",
"functions": "99.69",
"branches": "93.57",
"statements": "99.71"
}
18 changes: 9 additions & 9 deletions network.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@ export const ERC4626AdapterUSDCVersions = [
] as const;

export const RepayerProxy = DEFAULT_PROXY_TYPE + "Repayer";
export const PYUSDProcessorProxy = DEFAULT_PROXY_TYPE + "PYUSDProcessor";
export const USDCProcessorProxy = DEFAULT_PROXY_TYPE + "Processor";
export const USDGProcessorProxy = DEFAULT_PROXY_TYPE + "USDGProcessor";
export const PYUSDStashDexProcessorProxy = DEFAULT_PROXY_TYPE + "PYUSDStashDexProcessor";
export const USDCStashDexProcessorProxy = DEFAULT_PROXY_TYPE + "USDCStashDexProcessor";
export const USDGStashDexProcessorProxy = DEFAULT_PROXY_TYPE + "USDGStashDexProcessor";
const SUPPORTS_ONLY_USDC = false;

export enum Network {
Expand Down Expand Up @@ -625,37 +625,37 @@ export const networkConfig: NetworksConfig = {
TokenIn: Token.USDC,
TokenOut: Token.PYUSD,
FeeBps: 3,
Processor: PYUSDProcessorProxy,
Processor: PYUSDStashDexProcessorProxy,
},
{
TokenIn: Token.PYUSD,
TokenOut: Token.USDC,
FeeBps: 3,
Processor: USDCProcessorProxy,
Processor: USDCStashDexProcessorProxy,
},
{
TokenIn: Token.USDC,
TokenOut: Token.USDG,
FeeBps: 3,
Processor: USDGProcessorProxy,
Processor: USDGStashDexProcessorProxy,
},
{
TokenIn: Token.USDG,
TokenOut: Token.USDC,
FeeBps: 3,
Processor: USDCProcessorProxy,
Processor: USDCStashDexProcessorProxy,
},
{
TokenIn: Token.USDG,
TokenOut: Token.PYUSD,
FeeBps: 3,
Processor: PYUSDProcessorProxy,
Processor: PYUSDStashDexProcessorProxy,
},
{
TokenIn: Token.PYUSD,
TokenOut: Token.USDG,
FeeBps: 3,
Processor: USDGProcessorProxy,
Processor: USDGStashDexProcessorProxy,
},
],
},
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
"deploy-spark-stage-repayer-opmainnet": "STANDALONE_REPAYER_ENV=SparkStage hardhat run ./scripts/deployStandaloneRepayer.ts --network OP_MAINNET",
"deploy-usdc-processor-ethereum": "hardhat run ./scripts/deployUSDCProcessor.ts --network ETHEREUM",
"deploy-usdc-processor-ethereum-stage": "DEPLOY_TYPE=STAGE hardhat run ./scripts/deployUSDCProcessor.ts --network ETHEREUM",
"deploy-usdc-stashdex-processor-ethereum": "TARGET_ASSET_NAME=USDC run ./scripts/deployStashDexProcessor.ts --network ETHEREUM",
"deploy-usdc-stashdex-processor-ethereum-stage": "TARGET_ASSET_NAME=USDC DEPLOY_TYPE=STAGE hardhat run ./scripts/deployStashDexProcessor.ts --network ETHEREUM",
Comment on lines +75 to +76
"deploy-paxosoracle-ethereum": "hardhat run ./scripts/deployPaxosOracle.ts --network ETHEREUM",
"deploy-paxosoracle-ethereum-stage": "DEPLOY_TYPE=STAGE hardhat run ./scripts/deployPaxosOracle.ts --network ETHEREUM",
"upgrade-liquidityhub-basesepolia": "hardhat run ./scripts/upgradeLiquidityHub.ts --network BASE_SEPOLIA",
Expand Down Expand Up @@ -219,6 +221,8 @@
"dry:deploy-usdc-processor-ethereum": "DRY_RUN=ETHEREUM VERIFY=false ts-node --files ./scripts/deployUSDCProcessor.ts",
"dry:deploy-paxosoracle-ethereum": "DRY_RUN=ETHEREUM VERIFY=false ts-node --files ./scripts/deployPaxosOracle.ts",
"dry:deploy-paxosoracle-ethereum-stage": "DRY_RUN=ETHEREUM DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/deployPaxosOracle.ts",
"dry:deploy-usdc-stashdex-processor-ethereum-stage": "DRY_RUN=ETHEREUM VERIFY=false DEPLOY_TYPE=STAGE TARGET_ASSET_NAME=USDC ts-node --files ./scripts/deployStashDexProcessor.ts",
"dry:deploy-usdc-stashdex-processor-ethereum": "DRY_RUN=ETHEREUM VERIFY=false TARGET_ASSET_NAME=USDC ts-node --files ./scripts/deployStashDexProcessor.ts",
Comment on lines +224 to +225

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add pyusd and usdg processor deploy scripts. _Maybe follow #256 where processor deploy has configurable asset

"dry:upgrade-liquidityhub-basesepolia": "DRY_RUN=BASE_SEPOLIA VERIFY=false ts-node --files ./scripts/upgradeLiquidityHub.ts",
"dry:upgrade-liquidityhub-base": "DRY_RUN=BASE VERIFY=false ts-node --files ./scripts/upgradeLiquidityHub.ts",
"dry:upgrade-liquidityhub-base-stage": "DRY_RUN=BASE DEPLOY_TYPE=STAGE VERIFY=false ts-node --files ./scripts/upgradeLiquidityHub.ts",
Expand Down
3 changes: 1 addition & 2 deletions scripts/deployPaxosOracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ export async function main() {
assert(isSet(process.env.DEPLOY_ID), "DEPLOY_ID must be set");
const verifier = getVerifier(process.env.DEPLOY_ID);
console.log(`Deployment ID: ${process.env.DEPLOY_ID}`);
let id = "PaxosOracle";
const id = "PaxosOracle";

let network: Network;
let config: NetworkConfig;
console.log("Deploying PaxosOracle");
({network, config} = await getNetworkConfig());
if (!network) {
({network, config} = await getHardhatNetworkConfig());
id += "-DeployTest";
}
await logDeployers();

Expand Down
20 changes: 17 additions & 3 deletions scripts/deployStashDex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,13 @@ export async function main() {
assert(isSet(process.env.DEPLOY_ID), "DEPLOY_ID must be set");
const verifier = getVerifier(process.env.DEPLOY_ID);
console.log(`Deployment ID: ${process.env.DEPLOY_ID}`);
let id = "StashDex";

let network: Network;
let config: NetworkConfig;
console.log("Deploying StashDex");
({network, config} = await getNetworkConfig());
if (!network) {
({network, config} = await getHardhatNetworkConfig());
id += "-DeployTest";
}

await logDeployers();
Expand All @@ -42,6 +40,10 @@ export async function main() {
assertAddress(stashDexConfig.ConfigAdmin, "StashDex.ConfigAdmin must be an address");
assertAddress(stashDexConfig.Forwarder, "StashDex.Forwarder must be an address");

for (const {TokenOut} of stashDexConfig.Routes) {
assert(stashDexConfig.Pools[TokenOut], `Route tokenOut ${TokenOut} has no pool configured in StashDex.Pools`);
}

const oracle = await resolveXAddress(stashDexConfig.Oracle);
const receiver = await resolveXAddress(stashDexConfig.Receiver);

Expand Down Expand Up @@ -85,7 +87,7 @@ export async function main() {
initialPools,
initialRoutes,
],
id,
"StashDex",
verifier,
);

Expand All @@ -100,6 +102,18 @@ export async function main() {
console.table(initialRoutes);
}

if (initialPools.length > 0) {
const DIRECT_BORROW_ROLE = hre.ethers.encodeBytes32String("DIRECT_BORROW_ROLE");
const calldata = (await stashDex.grantRole.populateTransaction(DIRECT_BORROW_ROLE, stashDex.target)).data;
const poolTokenNames = Object.keys(stashDexConfig.Pools) as Token[];
console.log("NEXT STEPS — grant DIRECT_BORROW_ROLE on each pool so StashDex can borrow:");
console.log(`Calldata: ${calldata}`);
console.table(initialPools.map(({pool}, i) => ({
token: poolTokenNames[i],
to: pool,
})));
}

await verifier.verify(process.env.VERIFY === "true");
}

Expand Down
Loading
Loading