1

I've been trying to teach myself how to program smart contracts in Vyper and I was trying out a toy problem where the contract transfers USDC to itself and then mints some Compound USDC on Compound Finance.

I've been attempting to execute a function compound using MEW and it fails (out of gas or repeatedly failing error) before it even asks me to sign the transaction. I'm a little bit lost as to why it is failing and I can't seem to find a good tool to debug it. I've approved the contract with USDC AND cUSDC.

Here is my code:

from vyper.interfaces import ERC20

contract Compound():
    def redeem(quantity: uint256) -> uint256: modifying
    def mint(quantity: uint256) -> uint256: modifying
    def approve(target: address, quantity: uint256) -> bool: modifying
    def balanceOf(target: address) -> uint256: constant
    def transfer(to: address, tokens: uint256) -> bool: modifying

owner: address
cusdc_token: Compound
usdc_token: ERC20


@public
def __init__():
    self.owner = msg.sender
    self.cusdc_token = Compound(0x39AA39c021dfbaE8faC545936693aC917d5E7563)
    self.usdc_token = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48)

    self.usdc_token.approve(self.cusdc_token, 99998075535048195174647562)

@public
def kill():
    if msg.sender == self.owner:
        selfdestruct(self.owner)

@public
def compound(quantity: uint256, trans_cost: uint256):
    assert self.usdc_token.transferFrom(msg.sender, self, quantity)
    assert self.cusdc_token.mint(quantity) == 0

You'll notice that there is no code to return USDC or cUSDC to my account. That's just because I've been progressively deleting code to try and work out where its failing.

What am I missing? Any help would be much appreciated! I'm sure it's fairly straight forward!

  • There are only two statements in compound. Either could fail. Which one is failing? – smarx Aug 21 at 4:26
  • Assuming the second statement is failing (the call to mint), what value is it returning? That should tell you what the problem is. – smarx Aug 21 at 4:30
  • I think half my issue is that I am compiling in Vyper.Online and then using Mew to deploy and test. When I run it on Mew it doesn't actually tell me what the issue is - just comes up with an opaque 'repeatedly failing'. Doesn't even submit a transaction. Similar thing happened when I tried running a contract where I had forgotten to approve a token. I might need to set up a proper environment so I can catch that error - unless there is a way to display the error when using MEW? – simeon Aug 22 at 0:07
  • I would just delete the second line and see if it still fails. If not, then the second line is the problem. Drop the assert and just store the result somewhere. Then read it and see what it is. – smarx Aug 22 at 0:20
0

This is failing because you haven't entered the USDC market on Compound. This is difficult to do in Vyper because it takes a dynamic array as an input (which do not exist in Vyper). Therefore, you have to do some manual workarounds, by encoding and decoding into bytes.

You have to first do:

Comptroller troll = Comptroller(0xABCD...);
CToken[] memory cTokens = new CToken[](2);
cTokens[0] = CErc20(0x3FDA...);
cTokens[1] = CEther(0x3FDB...);
uint[] memory errors = troll.enterMarkets(cTokens);

Note: this is in Solidity. In Vyper, you should do something like:

@private
def uintResponse(byteArr: bytes[96]) -> uint256:
  """
  @notice Converts 96 byte array to a uint256
  @dev This assumes it is the output from a raw_call that takes form of offset + length + response
  @return uint256
  """
  # assumes output is coming from an uint[], therefore start at byte 64
  # because first two sets of 32 are offset & length
  start: int128 = 32*2

  # extract32 bytes of data
  extracted: bytes32 = extract32(byteArr, start, type=bytes32)

  # return converted 32 bytes to uint256
  return convert(extracted, uint256)

@private
def enterMarket(cToken: address, _troller: address):
  """
  @notice Enters specific compound market
  @dev This utilizes `raw_call` for specified cToken address as input as bytes
  @param cToken: compound cToken address
  """
    # get funcSig for compound comptroller function "enterMarkets" which
    # takes a dynamic address array
    funcSig: bytes[4] = method_id("enterMarkets(address[])", bytes[4])

    # There are 7 currently active markets on Compound
    # Convert specified cToken address to bytes32
    addrBytes: bytes[96] = concat(
                                convert(32, bytes32),
                                convert(1, bytes32),
                                convert(cToken, bytes32)
                              )

    # call should be in the form of:
    # funcSig + offset + lengthOfInputArray + inputArray
    full_data: bytes[100] = concat(funcSig, addrBytes)

    # returns byteArray of offset (32 bytes) + length (32 bytes) + addressArray (32 * 1)
    response: bytes[96] = raw_call(
                                _troller,           # Compound Comptroller address
                                full_data,          # funcSig + offset + lengthOfInputArray + inputArray
                                outsize=96,         # outsize = offset (32 bytes) + length (32 bytes) + addressArray (32 * 1)
                                gas=msg.gas,        # Pass msg.gas for call
                                value=0,            # Make sure to not send ETH
                                delegate_call=False # Not delegate_call
                              )

    # Error Code is returned which are uint256 so call uintResponse
    convertedResponse: uint256 = self.uintResponse(response)

    # Check no error
    assert convertedResponse == 0

This is all because "enterMarkets" function takes a dynamic array of addresses, which dynamic arrays are not currently supported in Vyper. If you want to enter all markets I have included the code around that below.

@private
def uint7ArrayResponse(byteArr: bytes[288]) -> uint256[7]:
  """
  @notice Converts 288 byte array to a uint256[7]
  @dev This assumes it is the output from a raw_call that takes form of offset + length + response
  @return uint256[7]
  """
  # initiate a uint256 array of length 7
  returnArr: uint256[7] = [0,0,0,0,0,0,0]

  # range to 9 to account for offset & length in byte input
  for i in range(9):
    # skip offset
    if i == 0:
      pass
    # skip length
    elif i == 1:
      pass
    # handle each uint256
    else:
      # multiply i * 32 to get starting byte to start extract32 from
      start: int128 = i*32

      # extract32 to bytes
      extracted: bytes32 = extract32(byteArr, start, type=bytes32)

      # set uint256 array index to extracted uint256, accounting for ranging over
      # 9 instead of 7 (thus -2)
      returnArr[i-2] = convert(extracted, uint256)
  return returnArr

@public
def enterAllMarkets(_cTokens: address[7], _troller: address):
  """
  @notice Enters all known compound markets
  @dev This utilizes `raw_call` with each cToken address as inputs as bytes
  """
  # get funcSig for compound comptroller function "enterMarkets" which
  # takes a dynamic address array
  funcSig: bytes[4] = method_id("enterMarkets(address[])", bytes[4])

  # There are 7 currently active markets on Compound
  # Convert each cToken address to bytes32 and concatenate them
  addrBytes: bytes[288] = concat(
                                convert(32, bytes32), #offset
                                convert(7, bytes32), #len
                                convert(_cTokens[0], bytes32),
                                convert(_cTokens[1], bytes32),
                                convert(_cTokens[2], bytes32),
                                convert(_cTokens[3], bytes32),
                                convert(_cTokens[4], bytes32),
                                convert(_cTokens[5], bytes32),
                                convert(_cTokens[6], bytes32)
                                )
  # call should be in the form of:
  # funcSig + offset + lengthOfInputArray + inputArray
  full_data: bytes[292] = concat(funcSig, addrBytes)

  # returns byteArray of offset (32 bytes) + length (32 bytes) + addressArray (32 * 7)
  response: bytes[288]= raw_call(
                              _troller,           # Compound Comptroller address
                              full_data,          # funcSig + offset + lengthOfInputArray + inputArray
                              outsize=288,        # outsize = offset (32 bytes) + length (32 bytes) + addressArray (32 * 7)
                              gas=msg.gas,        # Pass msg.gas for call
                              value=0,            # Make sure to not send ETH
                              delegate_call=False # Not delegate_call
                           )


  # Error Codes are returned which are uint256 so call uint7ArrayResponse
  responseArr: uint256[7] = self.uint7ArrayResponse(response)

  # No_Error response is equal to 0, so create an array that is equivalent to no errors
  successArr: uint256[7] = [0,0,0,0,0,0,0]

  # Iterate through and check that there were no errors for each market
  for i in range(7):
    assert responseArr[i] == successArr[i]

Once you have entered the market, then you should be able to mint. Dont forget to keep your approval amounts up to date as well. All in all, highly recommend using solidity for this task until Vyper supports variable length (but fixed via array[minlen:maxlen]) arrays, which should make this a lot easier.

  • Ah, so in the end it was specific to Compound. Awesome thanks! – simeon Sep 1 at 0:49
2

So currently vyper is being overprotective not allowing modifying calls from assert statements. See https://github.com/ethereum/vyper/issues/1468 and https://github.com/ethereum/vyper/issues/1150 for the VIP introducing the feature.

assert self.usdc_token.transferFrom(msg.sender, self, quantity)

This will be fixed by introduced assert_modifiable() builtin function, that will allow assert modifiable.

res: uint256 = self.usdc_token.transferFrom(msg.sender, self, quantity)
assert res > 0

Would be the correct way to do it currently. Also note that Vyper always asserts the CALL transaction being succesful - meaning if you do not care about the data coming from mint() you can just do self.usdc_token.transferFrom(msg.sender, self, quantity).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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