generated from Kwenta/foundry-scaffold
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
π·π»ββοΈ add local version of zap
- Loading branch information
1 parent
692dc4e
commit 28eb0e0
Showing
6 changed files
with
447 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
pragma solidity 0.8.20; | ||
|
||
import {IERC20} from "./interfaces/IERC20.sol"; | ||
import {ISpotMarketProxy} from "./interfaces/ISpotMarketProxy.sol"; | ||
import {ZapErrors} from "./ZapErrors.sol"; | ||
import {ZapEvents} from "./ZapEvents.sol"; | ||
|
||
/// @title Zap contract for wrapping/unwrapping $USDC into $sUSD | ||
/// via Synthetix v3 Andromeda Spot Market | ||
/// @author JaredBorders ([email protected]) | ||
abstract contract Zap is ZapErrors, ZapEvents { | ||
/*////////////////////////////////////////////////////////////// | ||
CONSTANTS | ||
//////////////////////////////////////////////////////////////*/ | ||
|
||
/// @notice keccak256 hash of expected name of $sUSDC synth | ||
/// @dev pre-computed to save gas during deployment: | ||
/// keccak256(abi.encodePacked("Synthetic USD Coin Spot Market")) | ||
bytes32 internal constant _HASHED_SUSDC_NAME = | ||
0xdb59c31a60f6ecfcb2e666ed077a3791b5c753b5a5e8dc5120f29367b94bbb22; | ||
|
||
/*////////////////////////////////////////////////////////////// | ||
IMMUTABLES | ||
//////////////////////////////////////////////////////////////*/ | ||
|
||
/// @notice $USDC token contract address | ||
IERC20 internal immutable _USDC; | ||
|
||
/// @notice $sUSD token/synth contract address | ||
IERC20 internal immutable _SUSD; | ||
|
||
/// @notice $sUSDC token/synth contract address | ||
IERC20 internal immutable _SUSDC; | ||
|
||
/// @notice Synthetix v3 Spot Market ID for $sUSDC | ||
uint128 internal immutable _SUSDC_SPOT_MARKET_ID; | ||
|
||
/// @notice Synthetix v3 Spot Market Proxy contract address | ||
ISpotMarketProxy internal immutable _SPOT_MARKET_PROXY; | ||
|
||
/// @notice used to adjust $USDC decimals | ||
uint256 internal immutable _DECIMALS_FACTOR; | ||
|
||
/*////////////////////////////////////////////////////////////// | ||
CONSTRUCTOR | ||
//////////////////////////////////////////////////////////////*/ | ||
|
||
/// @notice Zap constructor | ||
/// @dev will revert if any of the addresses are zero | ||
/// @dev will revert if the Synthetix v3 Spot Market ID for | ||
/// $sUSDC is incorrect | ||
/// @param _usdc $USDC token contract address | ||
/// @param _susd $sUSD token contract address | ||
/// @param _spotMarketProxy Synthetix v3 Spot Market Proxy | ||
/// contract address | ||
/// @param _sUSDCId Synthetix v3 Spot Market ID for $sUSDC | ||
constructor( | ||
address _usdc, | ||
address _susd, | ||
address _spotMarketProxy, | ||
uint128 _sUSDCId | ||
) { | ||
if (_usdc == address(0)) revert USDCZeroAddress(); | ||
if (_susd == address(0)) revert SUSDZeroAddress(); | ||
if (_spotMarketProxy == address(0)) revert SpotMarketZeroAddress(); | ||
|
||
_USDC = IERC20(_usdc); | ||
_SUSD = IERC20(_susd); | ||
_SPOT_MARKET_PROXY = ISpotMarketProxy(_spotMarketProxy); | ||
|
||
_DECIMALS_FACTOR = 10 ** (18 - IERC20(_usdc).decimals()); | ||
|
||
if ( | ||
keccak256(abi.encodePacked(_SPOT_MARKET_PROXY.name(_sUSDCId))) | ||
!= _HASHED_SUSDC_NAME | ||
) revert InvalidIdSUSDC(_sUSDCId); | ||
|
||
// id of $sUSDC is verified to be correct via the above | ||
// name comparison check | ||
_SUSDC_SPOT_MARKET_ID = _sUSDCId; | ||
_SUSDC = IERC20(_SPOT_MARKET_PROXY.getSynth(_sUSDCId)); | ||
} | ||
|
||
/*////////////////////////////////////////////////////////////// | ||
ZAP IN | ||
//////////////////////////////////////////////////////////////*/ | ||
|
||
/// @notice wrap $USDC into $sUSD | ||
/// @dev call will result in $sUSD minted to this contract | ||
/// @dev override this function to include additional logic | ||
/// @dev wrapping $USDC requires sufficient Zap | ||
/// contract $USDC allowance and results in a | ||
/// 1:1 ratio in terms of value | ||
/// @dev assumes zero fees when | ||
/// wrapping/unwrapping/selling/buying | ||
/// @param _amount is the amount of $USDC to wrap | ||
function _zapIn(uint256 _amount) | ||
internal | ||
virtual | ||
returns (uint256 adjustedAmount) | ||
{ | ||
// transfer $USDC to the Zap contract | ||
if (!_USDC.transferFrom(msg.sender, address(this), _amount)) { | ||
revert TransferFailed( | ||
address(_USDC), msg.sender, address(this), _amount | ||
); | ||
} | ||
|
||
// allocate $USDC allowance to the Spot Market Proxy | ||
if (!_USDC.approve(address(_SPOT_MARKET_PROXY), _amount)) { | ||
revert ApprovalFailed( | ||
address(_USDC), | ||
address(this), | ||
address(_SPOT_MARKET_PROXY), | ||
_amount | ||
); | ||
} | ||
|
||
/// @notice $USDC may use non-standard decimals | ||
/// @dev adjustedAmount is the amount of $sUSDC | ||
/// expected to receive from wrapping | ||
/// @dev Synthetix synths use 18 decimals | ||
/// @custom:example if $USDC has 6 decimals, | ||
/// and $sUSD and $sUSDC have 18 decimals, | ||
/// then, 1e12 $sUSD/$sUSDC = 1 $USDC | ||
adjustedAmount = _amount * _DECIMALS_FACTOR; | ||
|
||
/// @notice wrap $USDC into $sUSDC | ||
/// @dev call will result in $sUSDC minted/transferred | ||
/// to the Zap contract | ||
_SPOT_MARKET_PROXY.wrap({ | ||
marketId: _SUSDC_SPOT_MARKET_ID, | ||
wrapAmount: _amount, | ||
minAmountReceived: adjustedAmount | ||
}); | ||
|
||
// allocate $sUSDC allowance to the Spot Market Proxy | ||
if (!_SUSDC.approve(address(_SPOT_MARKET_PROXY), adjustedAmount)) { | ||
revert ApprovalFailed( | ||
address(_SUSDC), | ||
address(this), | ||
address(_SPOT_MARKET_PROXY), | ||
adjustedAmount | ||
); | ||
} | ||
|
||
/// @notice sell $sUSDC for $sUSD | ||
/// @dev call will result in $sUSD minted/transferred | ||
/// to the Zap contract | ||
_SPOT_MARKET_PROXY.sell({ | ||
marketId: _SUSDC_SPOT_MARKET_ID, | ||
synthAmount: adjustedAmount, | ||
minUsdAmount: adjustedAmount, | ||
referrer: address(0) | ||
}); | ||
|
||
emit ZappedIn({amountWrapped: _amount, amountMinted: adjustedAmount}); | ||
} | ||
|
||
/*////////////////////////////////////////////////////////////// | ||
ZAP OUT | ||
//////////////////////////////////////////////////////////////*/ | ||
|
||
/// @notice unwrap $USDC from $sUSD | ||
/// @dev call will result in $USDC transferred to this contract | ||
/// @dev override this function to include additional logic | ||
/// @dev unwrapping may result in a loss of precision: | ||
/// unwrapping (1e12 + n) $sUSDC results in 1 $USDC | ||
/// when n is a number less than 1e12; n $sUSDC is lost | ||
/// @param _amount is the amount of $sUSD to sell | ||
/// for $sUSDC and then unwrap | ||
function _zapOut(uint256 _amount) | ||
internal | ||
virtual | ||
returns (uint256 adjustedAmount) | ||
{ | ||
/// @notice prior to unwrapping, ensure that there | ||
/// is enough $sUSDC to unwrap | ||
/// @custom:example if $USDC has 6 decimals, and | ||
/// $sUSDC has greater than 6 decimals, | ||
/// then it is possible that the amount of | ||
/// $sUSDC to unwrap is less than 1 $USDC; | ||
/// this contract will prevent such cases | ||
/// @dev if $USDC has 6 decimals, and $sUSDC has 18 decimals, | ||
/// precision may be lost | ||
if (_amount < _DECIMALS_FACTOR) { | ||
revert InsufficientAmount(_amount); | ||
} | ||
|
||
// allocate $sUSD allowance to the Spot Market Proxy | ||
if (!_SUSD.approve(address(_SPOT_MARKET_PROXY), _amount)) { | ||
revert ApprovalFailed( | ||
address(_SUSD), | ||
address(this), | ||
address(_SPOT_MARKET_PROXY), | ||
_amount | ||
); | ||
} | ||
|
||
/// @notice buy $sUSDC with $sUSD | ||
/// @dev call will result in $sUSDC minted/transferred | ||
/// to the Zap contract | ||
_SPOT_MARKET_PROXY.buy({ | ||
marketId: _SUSDC_SPOT_MARKET_ID, | ||
usdAmount: _amount, | ||
minAmountReceived: _amount, | ||
referrer: address(0) | ||
}); | ||
|
||
// allocate $sUSDC allowance to the Spot Market Proxy | ||
if (!_SUSDC.approve(address(_SPOT_MARKET_PROXY), _amount)) { | ||
revert ApprovalFailed( | ||
address(_SUSDC), | ||
address(this), | ||
address(_SPOT_MARKET_PROXY), | ||
_amount | ||
); | ||
} | ||
|
||
/// @notice $USDC might use non-standard decimals | ||
/// @dev adjustedAmount is the amount of $USDC | ||
/// expected to receive from unwrapping | ||
/// @custom:example if $USDC has 6 decimals, | ||
/// and $sUSD and $sUSDC have 18 decimals, | ||
/// then, 1e12 $sUSD/$sUSDC = 1 $USDC | ||
adjustedAmount = _amount / _DECIMALS_FACTOR; | ||
|
||
/// @notice unwrap $USDC via burning $sUSDC | ||
/// @dev call will result in $USDC minted/transferred | ||
/// to the Zap contract | ||
_SPOT_MARKET_PROXY.unwrap({ | ||
marketId: _SUSDC_SPOT_MARKET_ID, | ||
unwrapAmount: _amount, | ||
minAmountReceived: adjustedAmount | ||
}); | ||
|
||
emit ZappedOut({amountBurned: _amount, amountUnwrapped: adjustedAmount}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
pragma solidity 0.8.20; | ||
|
||
/// @title Zap contract errors | ||
/// @author JaredBorders ([email protected]) | ||
contract ZapErrors { | ||
/// @notice thrown when $USDC address is zero | ||
/// @dev only can be thrown in during Zap deployment | ||
error USDCZeroAddress(); | ||
|
||
/// @notice thrown when $sUSD address is zero | ||
/// @dev only can be thrown in during Zap deployment | ||
error SUSDZeroAddress(); | ||
|
||
/// @notice thrown when Synthetix v3 Spot Market Proxy | ||
/// address is zero | ||
/// @dev only can be thrown in during Zap deployment | ||
error SpotMarketZeroAddress(); | ||
|
||
/// @notice thrown when the given Synthetix v3 Spot Market ID | ||
/// for $sUSDC is incorrect; querying the Spot Market Proxy | ||
/// contract for the name of the Spot Market ID returns | ||
/// a different name than expected | ||
/// @dev only can be thrown in during Zap deployment | ||
/// @param id Synthetix v3 Spot Market ID for $sUSDC | ||
error InvalidIdSUSDC(uint128 id); | ||
|
||
/// @notice thrown when a given token transfer fails | ||
/// @param token address of the token contract | ||
/// @param from address of the sender | ||
/// @param to address of the recipient | ||
/// @param amount amount of tokens to transfer | ||
error TransferFailed( | ||
address token, address from, address to, uint256 amount | ||
); | ||
|
||
/// @notice thrown when a given token approval fails | ||
/// @param token address of the token contract | ||
/// @param owner address of the token owner | ||
/// @param spender address of the spender | ||
/// @param amount amount of tokens to approve | ||
error ApprovalFailed( | ||
address token, address owner, address spender, uint256 amount | ||
); | ||
|
||
/// @notice thrown when the given amount is insufficient | ||
/// due to decimals adjustment | ||
/// @param amount amount of tokens to transfer | ||
/// @custom:example if $USDC has 6 decimals, and | ||
/// $sUSDC has greater than 6 decimals, | ||
/// then it is possible that the amount of | ||
/// $sUSDC to unwrap is less than 1 $USDC | ||
error InsufficientAmount(uint256 amount); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
pragma solidity 0.8.20; | ||
|
||
/// @title Zap contract events | ||
/// @author JaredBorders ([email protected]) | ||
contract ZapEvents { | ||
/// @notice emitted after successful $USDC -> $sUSD zap | ||
/// @param amountWrapped amount of $USDC wrapped | ||
/// @param amountMinted amount of $sUSD minted | ||
event ZappedIn(uint256 amountWrapped, uint256 amountMinted); | ||
|
||
/// @notice emitted after successful $sUSD -> $USDC zap | ||
/// @param amountBurned amount of $sUSD burned | ||
/// @param amountUnwrapped amount of $USDC unwrapped | ||
event ZappedOut(uint256 amountBurned, uint256 amountUnwrapped); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
pragma solidity 0.8.20; | ||
|
||
/// @title Reduced Interface of the ERC20 standard as defined in the EIP | ||
/// @author OpenZeppelin | ||
interface IERC20 { | ||
/// @dev Returns the number of decimals used to get its user representation. | ||
/// For example, if `decimals` equals `2`, a balance of `505` tokens should | ||
/// be displayed to a user as `5.05` (`505 / 10 ** 2`). | ||
function decimals() external view returns (uint8); | ||
|
||
/// @dev Returns the amount of tokens owned by `account`. | ||
function balanceOf(address account) external view returns (uint256); | ||
|
||
/// @dev Moves `amount` tokens from the caller's account to `to` | ||
/// @param to The address of the recipient | ||
/// @param amount The amount of tokens to transfer | ||
/// @return a boolean value indicating whether the operation succeeded | ||
/// Emits a {Transfer} event | ||
function transfer(address to, uint256 amount) external returns (bool); | ||
|
||
/// @dev Sets `amount` as the allowance of `spender` over the caller's tokens | ||
/// @param spender The address of the account to grant the allowance | ||
/// @param amount The amount of tokens to allow | ||
/// @return a boolean value indicating whether the operation succeeded | ||
/// Emits an {Approval} event. | ||
function approve(address spender, uint256 amount) external returns (bool); | ||
|
||
/// @dev Moves `amount` tokens from `from` to `to` using the | ||
/// allowance mechanism. `amount` is then deducted from the caller's | ||
/// allowance | ||
/// @param from The address of the sender | ||
/// @param to The address of the recipient | ||
/// @param amount The amount of tokens to transfer | ||
/// @return a boolean value indicating whether the operation succeeded. | ||
/// Emits a {Transfer} event | ||
function transferFrom(address from, address to, uint256 amount) | ||
external | ||
returns (bool); | ||
} |
Oops, something went wrong.