Skip to content

Commit

Permalink
contract: DFS: no approvals
Browse files Browse the repository at this point in the history
  • Loading branch information
dcposch committed Sep 17, 2024
1 parent e13d9d0 commit 0f07013
Show file tree
Hide file tree
Showing 8 changed files with 40 additions and 37 deletions.
3 changes: 1 addition & 2 deletions packages/contract/src/DaimoAccountV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -447,11 +447,10 @@ contract DaimoAccountV2 is IAccount, Initializable, IERC1271, ReentrancyGuard {
if (address(tokenIn) == address(0)) {
value = amountIn; // native token
} else {
tokenIn.forceApprove(address(swapper), amountIn);
tokenIn.safeTransfer(address(swapper), amountIn);
}
amountOut = swapper.swapToCoin{value: value}({
tokenIn: tokenIn,
amountIn: amountIn,
tokenOut: tokenOut,
extraData: extraData
});
Expand Down
22 changes: 17 additions & 5 deletions packages/contract/src/DaimoFlexSwapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import "./interfaces/IDaimoSwapper.sol";
/// @author The Daimo team
/// @custom:security-contact [email protected]
///
/// For security, this contract never holds any tokens (except during a swap)
/// and does not require any token approvals.
///
/// Starts by quoting an accurate reference price from any input (token, amount)
/// to a list of supported output stablecoins using Uniswap V3 TWAP/TWALs. See
/// https://uniswap.org/whitepaper-v3.pdf for more on TWAP and TWAL.
Expand Down Expand Up @@ -166,17 +169,28 @@ contract DaimoFlexSwapper is
// ----- PUBLIC FUNCTIONS -----

/// Swap input to output token at a fair price. Input token 0x0 refers to
/// the native token, eg ETH. Output token cannot be 0x0.
/// the native token, eg ETH. Output token cannot be 0x0. To call this, you
/// must first send the input amount to the contract. This must be done
/// within a single transaction. (Much like the Uniswap UniversalRouter.)
function swapToCoin(
IERC20 tokenIn,
uint256 amountIn,
IERC20 tokenOut,
bytes calldata extraData
) public payable returns (uint256 swapAmountOut) {
// Input checks. Input token 0x0 = native token, output must be ERC-20.
require(tokenIn != tokenOut, "DFS: input token = output token");
require(address(tokenOut) != address(0), "DFS: output token = 0x0");
require(isOutputToken[tokenOut], "DFS: unsupported output token");

// Get input amount
uint256 amountIn;
if (address(tokenIn) == address(0)) {
require(msg.value > 0, "DFS: missing msg.value");
amountIn = msg.value;
} else {
require(msg.value == 0, "DFS: unexpected msg.value");
amountIn = tokenIn.balanceOf(address(this));
}
require(amountIn < _MAX_UINT128, "DFS: amountIn too large");
DaimoFlexSwapperExtraData memory extra;
extra = abi.decode(extraData, (DaimoFlexSwapperExtraData));
Expand All @@ -193,11 +207,9 @@ contract DaimoFlexSwapper is
bytes memory callData = extra.callData;
uint256 callValue = 0;
if (address(tokenIn) == address(0)) {
require(msg.value == amountIn, "DFS: incorrect msg.value");
callValue = amountIn;
} else {
require(msg.value == 0, "DFS: unexpected msg.value");
tokenIn.safeTransferFrom(msg.sender, callDest, amountIn);
tokenIn.safeTransfer(callDest, amountIn);
}

// Execute swap
Expand Down
8 changes: 4 additions & 4 deletions packages/contract/src/interfaces/IDaimoSwapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ pragma solidity ^0.8.12;
import "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";

/// Swaps assets automatically. More precisely, it lets any market maker swap
/// swap tokens for a destination token, ensuring a fair price. The input comes
/// from msg.sender (which must have already approved) and output goes to same.
/// swap tokens for a destination token, ensuring a fair price. The input ETH or
/// ERC-20 tokens must be sent to swapper contract ahead of calling swapToCoin,
/// in the same transaction. Output tokens are sent to msg.sender. IDaimoSwapper
/// is used by other contracts; it can't be used from an EOA.
interface IDaimoSwapper {
/// Called to swap tokenIn to tokenOut. Ensures fair price or reverts.
/// @param tokenIn input ERC-20 token, 0x0 for native token
/// @param amountIn amount to swap. For native token, must match msg.value
/// @param tokenOut output ERC-20 token, cannot be 0x0
/// @param extraData swap route or similar, depending on implementation
function swapToCoin(
IERC20 tokenIn,
uint256 amountIn,
IERC20 tokenOut,
bytes calldata extraData
) external payable returns (uint256 amountOut);
Expand Down
3 changes: 1 addition & 2 deletions packages/contract/test/dummy/DaimoDummySwapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ contract DummySwapper is IDaimoSwapper {

function swapToCoin(
IERC20 tokenIn,
uint256 amountIn,
IERC20 tokenOut,
bytes calldata extraData
) external payable returns (uint256 amountOut) {
Expand All @@ -48,7 +47,7 @@ contract DummySwapper is IDaimoSwapper {

require(tokenIn == expectedTokenIn, "wrong tokenIn");
require(tokenOut == expectedTokenOut, "wrong tokenOut");
tokenIn.transferFrom(msg.sender, address(this), amountIn);
uint256 amountIn = tokenIn.balanceOf(address(this));

if (extraData.length > 0) {
// Call the reentrant swapAndTip() function
Expand Down
8 changes: 1 addition & 7 deletions packages/contract/test/uniswap/DaimoFlexSwapper.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ contract SwapperTest is Test {
deal(address(weth), alice, 1e18);
deal(address(degen), alice, amountIn);

degen.approve(address(swapper), amountIn);

bytes memory swapCallData = abi.encodeWithSignature(
"exactInput((bytes,address,uint256,uint256))",
ExactInputParams({
Expand All @@ -94,9 +92,9 @@ contract SwapperTest is Test {
})
);

degen.transfer(address(swapper), amountIn);
uint256 amountOut = swapper.swapToCoin({
tokenIn: degen,
amountIn: amountIn,
tokenOut: usdc,
extraData: abi.encode(
DaimoFlexSwapper.DaimoFlexSwapperExtraData({
Expand Down Expand Up @@ -153,7 +151,6 @@ contract SwapperTest is Test {
vm.expectRevert(bytes("DFS: insufficient output"));
swapper.swapToCoin{value: 1 ether}({
tokenIn: IERC20(address(0)),
amountIn: 1 ether,
tokenOut: weth,
extraData: abi.encode(
DaimoFlexSwapper.DaimoFlexSwapperExtraData({
Expand All @@ -166,7 +163,6 @@ contract SwapperTest is Test {
// 1:1 ETH to WETH = allowed
uint256 amountOut = swapper.swapToCoin{value: 1 ether}({
tokenIn: IERC20(address(0)),
amountIn: 1 ether,
tokenOut: weth,
extraData: abi.encode(
DaimoFlexSwapper.DaimoFlexSwapperExtraData({
Expand All @@ -185,7 +181,6 @@ contract SwapperTest is Test {
vm.expectRevert(bytes("DFS: input token = output token"));
swapper.swapToCoin({
tokenIn: weth,
amountIn: 1 ether,
tokenOut: weth,
extraData: abi.encode(
DaimoFlexSwapper.DaimoFlexSwapperExtraData({
Expand Down Expand Up @@ -221,7 +216,6 @@ contract SwapperTest is Test {

amountOut = swapper.swapToCoin{value: amountIn}({
tokenIn: IERC20(address(0)), // ETH
amountIn: amountIn,
tokenOut: usdc,
extraData: abi.encode(
DaimoFlexSwapper.DaimoFlexSwapperExtraData({
Expand Down
22 changes: 11 additions & 11 deletions packages/contract/test/uniswap/Quoter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,42 +97,42 @@ contract QuoterTest is Test {
// $3000.00 = 1 ETH, wrong price = block swap
fakeFeedETHUSD.setPrice(300000, 2);
vm.expectRevert(bytes("DFS: quote sanity check failed"));
swapper.swapToCoin(eth, 1 ether, usdc, emptySwapData());
swapper.swapToCoin{value: 1 ether}(eth, usdc, emptySwapData());

// $3450.00 = 1 ETH, ~correct price = OK, attempt swap
fakeFeedETHUSD.setPrice(345000, 2);
vm.expectRevert(bytes("DFS: swap produced no output"));
swapper.swapToCoin{value: 1 ether}(eth, 1 ether, usdc, emptySwapData());
swapper.swapToCoin{value: 1 ether}(eth, usdc, emptySwapData());

// Feed returning stale or missing price = block swap
fakeFeedETHUSD.setPrice(0, 0);
vm.expectRevert(bytes("DFS: CL price <= 0"));
swapper.swapToCoin{value: 1 ether}(eth, 1 ether, usdc, emptySwapData());
swapper.swapToCoin{value: 1 ether}(eth, usdc, emptySwapData());

// No price feed = OK, attempt swap
swapper.setKnownToken(weth, zeroToken);
vm.expectRevert(bytes("DFS: swap produced no output"));
swapper.swapToCoin{value: 1 ether}(eth, 1 ether, usdc, emptySwapData());
swapper.swapToCoin{value: 1 ether}(eth, usdc, emptySwapData());
}

function testChainlinkSanityCheckERC20() public {
deal(address(degen), address(this), 10e18);
degen.approve(address(swapper), 10e18);
degen.transfer(address(swapper), 1e18);

// $0.50 = 1 DEGEN, wrong price = block swap
fakeFeedDEGENUSD.setPrice(500, 3);
vm.expectRevert(bytes("DFS: quote sanity check failed"));
swapper.swapToCoin(degen, 1e18, usdc, emptySwapData());
swapper.swapToCoin(degen, usdc, emptySwapData());

// $0.0086 = 1 DEGEN, ~correct price = OK, attempt swap
fakeFeedDEGENUSD.setPrice(8600, 6);
vm.expectRevert(bytes("DFS: swap produced no output"));
swapper.swapToCoin(degen, 1 ether, usdc, emptySwapData());
swapper.swapToCoin(degen, usdc, emptySwapData());

// No price = OK, attempt swap
swapper.setKnownToken(degen, zeroToken);
vm.expectRevert(bytes("DFS: swap produced no output"));
swapper.swapToCoin(degen, 1e18, usdc, emptySwapData());
swapper.swapToCoin(degen, usdc, emptySwapData());
}

function testFlexSwapperQuote() public view {
Expand All @@ -154,14 +154,14 @@ contract QuoterTest is Test {
function testRebasingToken() public {
IERC20 usdm = IERC20(0x28eD8909de1b3881400413Ea970ccE377a004ccA);
deal(address(usdm), address(this), 123e18);
usdm.approve(address(swapper), 123e18);
usdm.transfer(address(swapper), 123e18);

// Protocol lets you unwrap 123 USDM for 122 USDC = within 1% of 1:1
bytes memory callData = fakeSwapData(usdm, usdc, 123e18, 122e6);

// Initially, swap fails because USDM is a rebasing token, no Uni price.
vm.expectRevert(bytes("DFS: no path found, amountOut 0"));
swapper.swapToCoin(usdm, 123e18, usdc, callData);
swapper.swapToCoin(usdm, usdc, callData);

// Give USDM a price feed + skip Uniswap. Swap should succeed.
FakeAggregator fakeFeedUSDM = new FakeAggregator();
Expand All @@ -174,7 +174,7 @@ contract QuoterTest is Test {
skipUniswap: true
})
);
swapper.swapToCoin(usdm, 123e18, usdc, callData);
swapper.swapToCoin(usdm, usdc, callData);

assertEq(usdm.balanceOf(address(this)), 0);
assertEq(usdc.balanceOf(address(this)), 122e6);
Expand Down
9 changes: 5 additions & 4 deletions packages/daimo-contract/script/gen-feeds.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assert, debugJson, optimismWETH } from "@daimo/common";
import { assert, assertNotNull, debugJson } from "@daimo/common";
import fs from "node:fs/promises";
import {
Address,
Expand All @@ -12,6 +12,7 @@ import {
import { arbitrum, base, mainnet, optimism, polygon } from "viem/chains";

import { ChainlinkFeed, PricedToken } from "./scriptModels";
import { getDAv2Chain } from "../dist";
import {
aggregatorV2V3InterfaceABI,
arbitrumWETH,
Expand All @@ -20,7 +21,7 @@ import {
erc20ABI,
ethereumWETH,
ForeignToken,
getAccountChain,
optimismWETH,
polygonWETH,
polygonWMATIC,
} from "../src";
Expand Down Expand Up @@ -289,7 +290,7 @@ async function priceTokens(
blockTimestamp = Number(block.timestamp);
}

const accChain = getAccountChain(token.chainId);
const accChain = getDAv2Chain(token.chainId);
const usdcAddr = accChain.bridgeCoin.token;

// Get Chainlink price feed for this token, if available
Expand Down Expand Up @@ -414,7 +415,7 @@ async function quoteToken({
chainId: token.chainId,
tokenSymbol: token.symbol,
tokenAddress,
tokenName: token.name,
tokenName: assertNotNull(token.name),
tokenDecimals: tokenDec,
logoURI: token.logoURI,
blockNumber,
Expand Down
2 changes: 0 additions & 2 deletions packages/daimo-contract/src/codegen/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2034,7 +2034,6 @@ export const daimoFlexSwapperABI = [
type: 'function',
inputs: [
{ name: 'tokenIn', internalType: 'contract IERC20', type: 'address' },
{ name: 'amountIn', internalType: 'uint256', type: 'uint256' },
{ name: 'tokenOut', internalType: 'contract IERC20', type: 'address' },
{ name: 'extraData', internalType: 'bytes', type: 'bytes' },
],
Expand Down Expand Up @@ -2943,7 +2942,6 @@ export const dummySwapperABI = [
type: 'function',
inputs: [
{ name: 'tokenIn', internalType: 'contract IERC20', type: 'address' },
{ name: 'amountIn', internalType: 'uint256', type: 'uint256' },
{ name: 'tokenOut', internalType: 'contract IERC20', type: 'address' },
{ name: 'extraData', internalType: 'bytes', type: 'bytes' },
],
Expand Down

0 comments on commit 0f07013

Please sign in to comment.