Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/GuardedEthTokenSwapper.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
// --- Chainlink Aggregator ---
interface AggregatorV3Interface {
function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80);
function decimals() external view returns (uint8);
}
// --- ERC20 / WETH / Uniswap v3 ---
interface IERC20 {
function decimals() external view returns (uint8);
function transfer(address, uint256) external returns (bool);
}
interface IWETH9 {
function deposit() external payable;
function approve(address, uint256) external returns (bool);
}
interface ISwapRouter {
struct ExactInputSingleParams {
address tokenIn;
address tokenOut;
uint24 fee;
address recipient;
uint256 deadline;
uint256 amountIn;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(ExactInputSingleParams calldata) external payable returns (uint256 amountOut);
}
library SafeTransfer {
function transfer(address token, address to, uint256 amount) internal returns (bool) {
(bool ok, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, amount));
return ok && (data.length == 0 || abi.decode(data, (bool)));
}
}
/**
* @title GuardedEthTokenSwapper
* @author ryley-o
* @notice Simplified ETH-only token swapper that uses TOKEN/ETH Chainlink price feeds.
* This version is optimized for ETH pairs only, removing USD complexity and reducing gas costs.
*
* ⚠️ USE AT YOUR OWN RISK. DO NOT USE THIS CONTRACT IF YOU DO NOT UNDERSTAND THE RISKS.
* ⚠️ THIS CONTRACT HAS NOT BEEN PROFESSIONALLY AUDITED.
* ⚠️ ONLY USE FUNDS YOU CAN AFFORD TO LOSE.
*
* @dev It guards against severe sandwich attacks by checking TOKEN/ETH prices from Chainlink oracles.
* The contract validates swap outcomes against oracle prices with configurable tolerance per token.
* All supported tokens must be configured by the owner before swapping is enabled.
*/
contract GuardedEthTokenSwapper is Ownable, ReentrancyGuard {
using SafeTransfer for address;
// Mainnet constants
address public constant UNISWAP_V3_ROUTER = 0xE592427A0AEce92De3Edee1F18E0157C05861564;
address public constant WETH9_ADDR = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
// Oracle staleness threshold (24 hours)
uint256 public constant MAX_ORACLE_STALENESS = 24 hours;
ISwapRouter public immutable router = ISwapRouter(UNISWAP_V3_ROUTER);
IWETH9 public immutable weth = IWETH9(WETH9_ADDR);
/**
* Simplified per-token registry entry (fits in 1 storage slot):
* - aggregator : 20 bytes (TOKEN/ETH Chainlink feed)
* - decimalsCache : 1 byte (aggregator.decimals())
* - feeTier : 3 bytes (Uniswap v3 fee: 500, 3000, 10000)
* - toleranceBps : 2 bytes (oracle ±accuracy in basis points, e.g. 200 = 2%)
* Total: 26 bytes (fits in 1 slot with 6 bytes padding)
*/
struct FeedInfo {
address aggregator; // 20 bytes - TOKEN/ETH price feed
uint8 decimalsCache; // 1 byte - cached decimals from aggregator
uint24 feeTier; // 3 bytes - Uniswap V3 fee tier
uint16 toleranceBps; // 2 bytes - price tolerance in basis points
// 6 bytes padding
}
mapping(address => FeedInfo) public feeds; // token => config
// Errors / events
error NoEthSent();
error FeedNotSet();
error FeeNotSet();
error OracleBad();
error OracleStale();
error InvalidSlippage();
error ApproveFailed();
error TransferFailed();
event FeedSet(
address indexed token, address indexed aggregator, uint8 decimals, uint24 feeTier, uint16 toleranceBps
);
event FeedRemoved(address indexed token);
event Swapped(
address indexed user,
address indexed token,
uint256 ethIn,
uint256 tokensOut,
uint24 fee,
uint256 minOut,
uint256 tokenEthPrice
);
/**
* @notice Initializes the contract and sets the deployer as owner
* @dev No ETH/USD feed needed - we only use TOKEN/ETH feeds
*/
constructor() Ownable(msg.sender) {
// Deployer becomes the owner via Ownable constructor
}
// --- Admin: set/update entries (token, aggregator, feeTier, toleranceBps) ---
/**
* @notice Configures price feeds and swap parameters for multiple tokens (owner only)
* @dev All arrays must be the same length. Each token gets a dedicated Chainlink feed and Uniswap config.
* @param tokens Array of ERC20 token addresses to configure
* @param aggregators Array of Chainlink TOKEN/ETH price feed addresses
* @param feeTiers Array of Uniswap V3 fee tiers (500=0.05%, 3000=0.30%, 10000=1.00%)
* @param toleranceBpsArr Array of price tolerance values in basis points (e.g., 200=2%)
* Requirements:
* - All arrays must have matching length
* - Token and aggregator addresses must be non-zero
* - Fee tiers must be 500, 3000, or 10000
* - Tolerance must be ≤ 2000 bps (20%)
*/
function setFeeds(
address[] calldata tokens,
address[] calldata aggregators,
uint24[] calldata feeTiers,
uint16[] calldata toleranceBpsArr
) external onlyOwner {
uint256 n = tokens.length;
require(n == aggregators.length && n == feeTiers.length && n == toleranceBpsArr.length, "len mismatch");
for (uint256 i; i < n; ++i) {
_setFeed(tokens[i], aggregators[i], feeTiers[i], toleranceBpsArr[i]);
}
}
function _setFeed(address token, address aggregator, uint24 feeTier, uint16 toleranceBps) internal {
require(token != address(0) && aggregator != address(0), "zero addr");
require(feeTier == 500 || feeTier == 3000 || feeTier == 10000, "bad fee");
require(toleranceBps <= 2000, "tolerance too high"); // <=20%
uint8 dec = AggregatorV3Interface(aggregator).decimals();
feeds[token] =
FeedInfo({aggregator: aggregator, decimalsCache: dec, feeTier: feeTier, toleranceBps: toleranceBps});
emit FeedSet(token, aggregator, dec, feeTier, toleranceBps);
}
// --- Admin: remove feed ---
/**
* @notice Removes price feed configuration for a token (owner only)
* @dev After removal, swaps for this token will revert with FeedNotSet error.
* Use this to disable support for a token without redeploying the contract.
* @param token The ERC20 token address to remove from supported tokens
*/
function removeFeed(address token) external onlyOwner {
require(feeds[token].aggregator != address(0), "feed not set");
delete feeds[token];
emit FeedRemoved(token);
}
// --- Simplified ETH-only swap ---
/**
* @param token ERC20 to buy (must have TOKEN/ETH feed configured).
* @param slippageBps Runtime slippage buffer (e.g., 200 = 2%).
* @param deadline Unix timestamp after which the transaction will revert.
* @return amountOut Tokens transferred to msg.sender.
*/
function swapEthForToken(address token, uint16 slippageBps, uint256 deadline)
external
payable
nonReentrant
returns (uint256 amountOut)
{
if (msg.value == 0) revert NoEthSent();
if (slippageBps > 10000) revert InvalidSlippage(); // Max 100%
if (deadline < block.timestamp) revert("deadline expired");
FeedInfo memory f = feeds[token];
if (f.aggregator == address(0)) revert FeedNotSet();
if (f.feeTier == 0) revert FeeNotSet();
// 1) Read TOKEN/ETH price with staleness check
(, int256 tokEthAns,, uint256 tokUpdatedAt,) = AggregatorV3Interface(f.aggregator).latestRoundData();
if (tokEthAns <= 0) revert OracleBad();
if (block.timestamp - tokUpdatedAt > MAX_ORACLE_STALENESS) revert OracleStale();
uint256 tokEthPrice = uint256(tokEthAns);
// 2) Compute expected tokens - standard TOKEN/ETH calculation
uint256 expectedTokens;
{
uint8 tokenDec = IERC20(token).decimals();
// TOKEN/ETH price means: tokEthPrice * 10^decimalsCache = tokens per 1 ETH
// For msg.value ETH: expectedTokens = msg.value * tokEthPrice * 10^tokenDec / (1e18 * 10^decimalsCache)
// Use precision scaling to avoid early division
uint256 scaledNumerator = msg.value * tokEthPrice * (10 ** tokenDec);
expectedTokens = scaledNumerator / (1e18 * (10 ** f.decimalsCache));
}
// 3) Conservative minOut using both user slippage and feed tolerance
uint256 totalBps = uint256(slippageBps) + uint256(f.toleranceBps);
if (totalBps > 10_000) totalBps = 10_000; // clamp
uint256 minOut = expectedTokens * (10_000 - totalBps) / 10_000;
// 4) Wrap ETH, approve, swap
weth.deposit{value: msg.value}();
if (!weth.approve(UNISWAP_V3_ROUTER, msg.value)) revert ApproveFailed();
ISwapRouter.ExactInputSingleParams memory p = ISwapRouter.ExactInputSingleParams({
tokenIn: WETH9_ADDR,
tokenOut: token,
fee: f.feeTier,
recipient: address(this),
deadline: deadline,
amountIn: msg.value,
amountOutMinimum: minOut,
sqrtPriceLimitX96: 0
});
amountOut = router.exactInputSingle(p);
// 5) Send tokens to caller
if (!SafeTransfer.transfer(token, msg.sender, amountOut)) revert TransferFailed();
emit Swapped(msg.sender, token, msg.value, amountOut, f.feeTier, minOut, tokEthPrice);
}
// --- View functions ---
/**
* @notice Returns the configuration for a given token
* @param token The ERC20 token address to query
* @return aggregator The Chainlink price feed address (zero if not configured)
* @return decimals The cached decimal count from the price feed
* @return feeTier The Uniswap V3 fee tier to use for swaps
* @return toleranceBps The price tolerance in basis points
*/
function getFeed(address token)
external
view
returns (address aggregator, uint8 decimals, uint24 feeTier, uint16 toleranceBps)
{
FeedInfo memory x = feeds[token];
return (x.aggregator, x.decimalsCache, x.feeTier, x.toleranceBps);
}
receive() external payable {
revert();
}
}
"
},
"lib/openzeppelin-contracts/contracts/access/Ownable.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)
pragma solidity ^0.8.20;
import {Context} from "../utils/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* The initial owner is set to the address provided by the deployer. This can
* later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
abstract contract Ownable is Context {
address private _owner;
/**
* @dev The caller account is not authorized to perform an operation.
*/
error OwnableUnauthorizedAccount(address account);
/**
* @dev The owner is not a valid owner account. (eg. `address(0)`)
*/
error OwnableInvalidOwner(address owner);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the address provided by the deployer as the initial owner.
*/
constructor(address initialOwner) {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
_checkOwner();
_;
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view virtual returns (address) {
return _owner;
}
/**
* @dev Throws if the sender is not the owner.
*/
function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby disabling any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Internal function without access restriction.
*/
function _transferOwnership(address newOwner) internal virtual {
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
"
},
"lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol)
pragma solidity ^0.8.20;
/**
* @dev Contract module that helps prevent reentrant calls to a function.
*
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
* available, which can be applied to functions to make sure there are no nested
* (reentrant) calls to them.
*
* Note that because there is a single `nonReentrant` guard, functions marked as
* `nonReentrant` may not call one another. This can be worked around by making
* those functions `private`, and then adding `external` `nonReentrant` entry
* points to them.
*
* TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,
* consider using {ReentrancyGuardTransient} instead.
*
* TIP: If you would like to learn more about reentrancy and alternative ways
* to protect against it, check out our blog post
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
*/
abstract contract ReentrancyGuard {
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
// The values being non-zero value makes deployment a bit more expensive,
// but in exchange the refund on every call to nonReentrant will be lower in
// amount. Since refunds are capped to a percentage of the total
// transaction's gas, it is best to keep them low in cases like this one, to
// increase the likelihood of the full refund coming into effect.
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
uint256 private _status;
/**
* @dev Unauthorized reentrant call.
*/
error ReentrancyGuardReentrantCall();
constructor() {
_status = NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
* Calling a `nonReentrant` function from another `nonReentrant`
* function is not supported. It is possible to prevent this from happening
* by making the `nonReentrant` function external, and making it call a
* `private` function that does the actual work.
*/
modifier nonReentrant() {
_nonReentrantBefore();
_;
_nonReentrantAfter();
}
function _nonReentrantBefore() private {
// On the first call to nonReentrant, _status will be NOT_ENTERED
if (_status == ENTERED) {
revert ReentrancyGuardReentrantCall();
}
// Any calls to nonReentrant after this point will fail
_status = ENTERED;
}
function _nonReentrantAfter() private {
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = NOT_ENTERED;
}
/**
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
* `nonReentrant` function in the call stack.
*/
function _reentrancyGuardEntered() internal view returns (bool) {
return _status == ENTERED;
}
}
"
},
"lib/openzeppelin-contracts/contracts/utils/Context.sol": {
"content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)
pragma solidity ^0.8.20;
/**
* @dev Provides information about the current execution context, including the
* sender of the transaction and its data. While these are generally available
* via msg.sender and msg.data, they should not be accessed in such a direct
* manner, since when dealing with meta-transactions the account sending and
* paying for execution may not be the actual sender (as far as an application
* is concerned).
*
* This contract is only required for intermediate, library-like contracts.
*/
abstract contract Context {
function _msgSender() internal view virtual returns (address) {
return msg.sender;
}
function _msgData() internal view virtual returns (bytes calldata) {
return msg.data;
}
function _contextSuffixLength() internal view virtual returns (uint256) {
return 0;
}
}
"
}
},
"settings": {
"remappings": [
"@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/",
"erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/",
"forge-std/=lib/forge-std/src/",
"halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/",
"openzeppelin-contracts/=lib/openzeppelin-contracts/"
],
"optimizer": {
"enabled": true,
"runs": 200
},
"metadata": {
"useLiteralContent": false,
"bytecodeHash": "ipfs",
"appendCBOR": true
},
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"devdoc",
"userdoc",
"metadata",
"abi"
]
}
},
"evmVersion": "prague",
"viaIR": true
}
}}
Submitted on: 2025-10-21 11:20:06
Comments
Log in to comment.
No comments yet.