2

I'm trying to create a simple Vyper contract that withdraws my USDC and converts this to a token that's passed through the argument list in the swap_usdc method.

I approve this contract to withdraw my USDC however, when attempting to call the swap_usdc method, the contract reverts.

I have no clue as to why and running out of ideas. Could it be that I'm not calling the UniswapV3 interfaces correctly? Any other ideas/help much appreciated.

Here are my

Input parameters: 5000000 (5 USDC)

Outtoken address: 0x8505b9d2254A7Ae468c0E9dd10Ccea3A837aef5c (Compound on Polygon)

Failed tx hash: 0x33db3c307813a307fd9086b41d3e3a92a5d601ba0a5c5b7c679cf32dbe9e2584

Code:

from vyper.interfaces import ERC20

USDC: constant(address) = 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174

UNIV3_ROUTER: constant(address) = 0xE592427A0AEce92De3Edee1F18E0157C05861564


struct ExactInputSingleParams:  # ISwapRouter
    tokenIn: address
    tokenOut: address
    fee: uint256
    recipient: address
    deadline: uint256
    amountIn: uint256
    amountOutMinimum: uint256
    sqrtPriceLimitX96: uint256

struct ExactOutputSingleParams:  # ISwapRouter
    tokenIn: address
    tokenOut: address
    fee: uint256
    recipient: address
    deadline: uint256
    amountOut: uint256
    amountInMaximum: uint256
    sqrtPriceLimitX96: uint256

interface ISwapRouter:
    # @uniswap-v3-periphery/SwapRouter.sol
    def exactInputSingle(params: ExactInputSingleParams) -> uint256: payable
    def exactOutputSingle(params: ExactOutputSingleParams) -> uint256: payable
    def refundETH(): nonpayable


@external
def swap_usdc(amount_in: uint256, outtoken: address, desination: address):
    ERC20(USDC).transferFrom(msg.sender,self,amount_in)
    result: uint256 = ISwapRouter(UNIV3_ROUTER).exactInputSingle(
        ExactInputSingleParams({
            tokenIn: USDC,
            tokenOut: outtoken,
            fee: convert(3000, uint256),
            recipient: desination,
            deadline: block.timestamp,
            amountIn: amount_in,
            amountOutMinimum: 0,
            sqrtPriceLimitX96: empty(uint256)
        })
    )

Even when I update code to below based on feedback from people below, i still get wrong keccak value

from vyper.interfaces import ERC20



UNISWAP: constant(address) = 0x93bcDc45f7e62f89a8e901DC4A0E2c6C427D9F25 #0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D ethereum


@external
def swap(_from: address, _to: address, _amount: uint256, _minout: uint256) -> Bytes[32]:
        ERC20(_from).transferFrom(msg.sender,self,_amount)
        ERC20(_from).approve(UNISWAP, _amount)
        
        result: Bytes[32] = raw_call(
        UNISWAP,
     concat(method_id("exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))"),
        convert(_from, bytes32),
        convert(_to, bytes32),
        convert(3000, bytes32),
        convert(msg.sender, bytes32),
        convert(block.timestamp, bytes32),
        convert(_amount, bytes32),
        convert(_minout, bytes32),
        convert(0, bytes32)
        ),
        is_delegate_call=False,
        max_outsize=32
        )

        assert convert(result,uint256) >= _minout, "less"
        return result
            
7
  • Can you add the input parameters you are using as well as the exact error message that you get returned in your question?
    – Rohan Nero
    Commented Sep 6, 2023 at 0:51
  • Have you called the approve function on the USDC token contract to allow your contract to spend the tokens? Commented Sep 6, 2023 at 8:21
  • You should add parameters, and either a failed transaction hash, or the failing calldata for simulation.
    – Maka
    Commented Sep 6, 2023 at 9:26
  • @RohanNero input parameters: amount_in 5000000 (5 USDC), outtoken address: 0x8505b9d2254A7Ae468c0E9dd10Ccea3A837aef5c (Compound on Polygon) destination my wallet address. I am compiling this and testing it in Remix in my browser, I simply get a contract revert error - I have approved the contract to withdraw my USDC (uint256 max). Commented Sep 6, 2023 at 9:43
  • 1
    @Maka failed tx hash: 0x33db3c307813a307fd9086b41d3e3a92a5d601ba0a5c5b7c679cf32dbe9e2584 pls see comment to RohanNero regarding tx params. Commented Sep 6, 2023 at 9:53

1 Answer 1

1

Sim is failing with:

"Error Message: Called function does not exist in the contract"

Looks like you are creating an incorrect call signature.

It should be 0x414bf389 the keccak of:

exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))

You are calling 0x53640a38 the keccak of:

exactInputSingle((address,address,uint256,address,uint256,uint256,uint256,uint256))

Fix the interface and try encoding the specific uint sizes convert(_literal, uint24) etc.

edit:

One way to handle this (as well as almost any other situation) is to raw_call:

interface IERC20:
  def approve(spender: address, amount: uint256) -> bool: nonpayable
  def transferFrom(sender: address, recipient: address, amount: uint256) -> bool: nonpayable

USDC: constant(address) = 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174
UNIV3_ROUTER: constant(address) = 0xE592427A0AEce92De3Edee1F18E0157C05861564

@external
def swap_usdc(amount_in: uint256, outtoken: address, destination: address, fee: uint256):
  IERC20(USDC).transferFrom(msg.sender, self, amount_in)
  IERC20(USDC).approve(UNIV3_ROUTER, amount_in)

  raw_call(
    UNIV3_ROUTER,
    concat(
      method_id("exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))"),
      convert(USDC, bytes32),
      convert(outtoken, bytes32),
      convert(fee, bytes32),
      convert(destination, bytes32),
      convert(block.timestamp, bytes32),
      convert(amount_in, bytes32),
      convert(0, bytes32),
      convert(0, bytes32)
    ),
    is_delegate_call=False,
  )

edit addressing updated issue:

The code above works. The issue you are having now is a result of changing the router address from the one in your first example. Using the v2 router won't work as it doesn't have the same functions.

Simply replace the contract address you have under UNISWAP with the one you used under UNIV3_ROUTER above that.

If you use the code exactly as in my example it works, I have also validated an example which is the same outside of not explicitly setting False for delegate call, but it is the default so effectively identical.

Validated contract example: https://polygonscan.com/address/0x870a986c7685494db8759a612368494a9be51fbe#code

Play with it if still having issues.


edit to respond to request in comment:

how does the contract change if we want to deposit native tokens to it?

The pools don't support eth, where eth is used it has to be wrapped first. With v3 there is a statement in the pay function which is called during the swap:

if (token == WETH9 && address(this).balance >= value)
  // pay with WETH9
  IWETH9(WETH9).deposit{value: value}(); // wrap only what is needed to pay
  IWETH9(WETH9).transfer(recipient, value);

Make the function payable, send the value on with the call, and use the 'weth' (wrapped native) token address.

interface IERC20:
  def approve(spender: address, amount: uint256) -> bool: nonpayable
  def transferFrom(sender: address, recipient: address, amount: uint256) -> bool: nonpayable

WETH: constant(address) = 0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270
UNIV3_ROUTER: constant(address) = 0xE592427A0AEce92De3Edee1F18E0157C05861564

# add payable
@payable
@external
def swap_eth(amount_in: uint256, outtoken: address, destination: address, fee: uint256):
  assert msg.value >= amount_in, 'Not enough eth supplied.' # remember to send an amount of the native coin

  raw_call(
    UNIV3_ROUTER,
    concat(
      method_id("exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))"),
      convert(WETH, bytes32),
      convert(outtoken, bytes32),
      convert(fee, bytes32),
      convert(destination, bytes32),
      convert(block.timestamp, bytes32),
      convert(amount_in, bytes32),
      convert(0, bytes32),
      convert(0, bytes32)
    ),
    value=msg.value  # pass native to the router
  )
9
  • 1
    Thank you, unfortunately, vyper doesnt seem to support uint24, uint160 so I've cast them as uint256- what can I do to make it work? Grateful for your help Commented Sep 6, 2023 at 21:05
  • See updated answer with example.
    – Maka
    Commented Sep 6, 2023 at 22:54
  • I'm still struggling.. still getting wrong keccak value despite using code above.. @maka dashboard.tenderly.co/shared/simulation/… Commented Sep 10, 2023 at 17:19
  • The code should have worked as was, double check what you have that isn't the same. Seems you are calling the wrong router, v2 which doesn't have that function. Call v3router.
    – Maka
    Commented Sep 10, 2023 at 18:15
  • Grateful as always @maka I’m using the same address for Uniswap as before - how can I call v3? Commented Sep 10, 2023 at 18:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.