Skip to content

contract: DFS: no approvals #1331

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
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
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 @@ -2033,7 +2033,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 @@ -2942,7 +2941,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