GithubHelp home page GithubHelp logo

balmy-protocol / permit2-adapter Goto Github PK

View Code? Open in Web Editor NEW
1.0 1.0 1.0 77 KB

This repository holds some adapter contracts that will allow to use Permit2 from contracts that don't support it natively

License: MIT License

Solidity 100.00%

permit2-adapter's Introduction

Universal Permit2 Adapter

Permit2 by Uniswap is an amazing development that will revolutionize the UX in Web3. However, it can be hard to migrate existing contracts to use this new way of transferring tokens.

To help in those cases, we've built the Universal Permit2 Adapter. It's basically an adapter that can be used to give Permit2 capabilities to existing contracts, without having to to re-deploy.

If you'd like to interact with these contracts, we suggest you use our SDK that already handles most of the complexities around it.

Usage

This is a list of the most frequently needed commands.

Build

Build the contracts:

$ forge build

Clean

Delete the build artifacts and cache directories:

$ forge clean

Compile

Compile the contracts:

$ forge build

Coverage

Get a test coverage report:

$ forge coverage

Format

Format the contracts:

$ forge fmt

Gas Usage

Get a gas report:

$ forge test --gas-report

Lint

Lint the contracts:

$ pnpm lint

Test

Run the tests:

$ forge test

Audit

This code has been audited by Omniscia.io. You can find the report here.

License

This project is licensed under MIT.

permit2-adapter's People

Contributors

0xsambugs avatar 0xkoaj avatar dependabot[bot] avatar

Stargazers

Perseverance Success avatar

Watchers

 avatar

Forkers

sam-goldman

permit2-adapter's Issues

[Omniscia Preliminary Report] [SPA-01M] Incorrect Assumption of Input

SPA-01M: Incorrect Assumption of Input

Type Severity Location
Logical Fault SwapPermit2Adapter.sol:L34, L40, L49

Description:

The _params.swapData payload supplied as part of a Address::functionCallWithValue invocation on the _params.swapper is not sanitized, meaning that it may perform an arbitrary operation rather than an exact-input swap.

Impact:

The _amountIn argument provided by a SwapPermit2Adapter::sellOrderSwap function may be incorrect as has not necessarily been consumed by the _params.swapper interaction.

Example:

/// @inheritdoc ISwapPermit2Adapter
function sellOrderSwap(SellOrderSwapParams calldata _params)
  public
  payable
  checkDeadline(_params.deadline)
  returns (uint256 _amountIn, uint256 _amountOut)
{
  // Take from caller
  PERMIT2.takeFromCaller(_params.tokenIn, _params.amountIn, _params.nonce, _params.deadline, _params.signature);

  // Max approve token in
  _params.tokenIn.maxApproveIfNecessary(_params.allowanceTarget);

  // Execute swap
  _params.swapper.functionCallWithValue(_params.swapData, msg.value);

  // Distribute token out
  _amountOut = _params.tokenOut.distributeTo(_params.transferOut);

  // Check min amount
  if (_amountOut < _params.minAmountOut) revert ReceivedTooLittleTokenOut(_amountOut, _params.minAmountOut);

  // Set amount in
  _amountIn = _params.amountIn;
}

Recommendation:

We advise the code to either sanitize the _params.swapData passed in if the swapper is known or to re-assess the _amountIn by evaluating whether any remainder of _params.tokenIn lies within the contract akin to SwapPermit2Adapter::buyOrderSwap.

[Omniscia Preliminary Report] [AEP-01M] Potential Increase of Functionality

AEP-01M: Potential Increase of Functionality

Type Severity Location
Logical Fault ArbitraryExecutionPermit2Adapter.sol:L86-L92

Description:

Given that the ArbitraryExecutionPermit2Adapter contract is meant to acquire the necessary funds from the caller and perform a sequence of calls with them, it is expected that any leftover funds are to be refunded to the caller.

The current code relies on a properly structured _transferOut entry that may ultimately not be supplied by the function's callers, leading to leftover funds being up for grabs by any party.

Impact:

While the function will behave as expected if used properly, we advise minimizing its reliance on input arguments to ensure that callers can interact with the contract in a safety-first manner rather than a flexibility-first approach.

Example:

/// @inheritdoc IArbitraryExecutionPermit2Adapter
function executeWithPermit(
  SinglePermit calldata _permit,
  AllowanceTarget[] calldata _allowanceTargets,
  ContractCall[] calldata _contractCalls,
  TransferOut[] calldata _transferOut,
  uint256 _deadline
)
  external
  payable
  checkDeadline(_deadline)
  returns (bytes[] memory _executionResults, uint256[] memory _tokenBalances)
{
  PERMIT2.takeFromCaller(_permit.token, _permit.amount, _permit.nonce, _deadline, _permit.signature);
  return _approveExecuteAndTransfer(_allowanceTargets, _contractCalls, _transferOut);
}

/// @inheritdoc IArbitraryExecutionPermit2Adapter
function executeWithBatchPermit(
  BatchPermit calldata _batchPermit,
  AllowanceTarget[] calldata _allowanceTargets,
  ContractCall[] calldata _contractCalls,
  TransferOut[] calldata _transferOut,
  uint256 _deadline
)
  external
  payable
  checkDeadline(_deadline)
  returns (bytes[] memory _executionResults, uint256[] memory _tokenBalances)
{
  PERMIT2.batchTakeFromCaller(_batchPermit.tokens, _batchPermit.nonce, _deadline, _batchPermit.signature);
  return _approveExecuteAndTransfer(_allowanceTargets, _contractCalls, _transferOut);
}

function _approveExecuteAndTransfer(
  AllowanceTarget[] calldata _allowanceTargets,
  ContractCall[] calldata _contractCalls,
  TransferOut[] calldata _transferOut
)
  internal
  returns (bytes[] memory _executionResults, uint256[] memory _tokenBalances)
{
  // Approve targets
  for (uint256 i; i < _allowanceTargets.length;) {
    IERC20(_allowanceTargets[i].token).maxApprove(_allowanceTargets[i].allowanceTarget);
    unchecked {
      ++i;
    }
  }

  // Call contracts
  _executionResults = new bytes[](_contractCalls.length);
  for (uint256 i; i < _contractCalls.length;) {
    _executionResults[i] =
      _contractCalls[i].target.functionCallWithValue(_contractCalls[i].data, _contractCalls[i].value);
    unchecked {
      ++i;
    }
  }

  // Distribute tokens
  _tokenBalances = new uint256[](_transferOut.length);
  for (uint256 i; i < _transferOut.length;) {
    _tokenBalances[i] = _transferOut[i].token.distributeTo(_transferOut[i].distribution);
    unchecked {
      ++i;
    }
  }
}

Recommendation:

We advise the code to either build an automatic _transferOut list from the supplied input tokens of an arbitrary call, or to validate that the _transferOut array contains at least all input tokens of an arbitrary call.

We consider either of the two approaches sufficient for alleviating this exhibit.

[Omniscia Preliminary Report] [AEP-03M] Race-Condition of Permit2 Approvals

AEP-03M: Race-Condition of Permit2 Approvals

Type Severity Location
Logical Fault ArbitraryExecutionPermit2Adapter.sol:L38-L39, L55-L56

Description:

The ArbitraryExecutionPermit2Adapter will integrate with the Permit2 contract in a non-standard way, consuming a direct permission of a transfer-from operation.

The contract is susceptible to on-chain conditions that would result in the compromise of a user's permitted funds.

To elaborate, the IPermit2::permitTransferFrom function can be invoked by an arbitrary party. As such, an ArbitraryExecutionPermit2Adapter::executeWithPermit function call can be detected by off-chain bots and its permit consumed by a direct call to the Permit2 contract.

As the ArbitraryExecutionPermit2Adapter permits arbitrary executions, the consumption of the permit can be coupled with a call to the contract that siphons those funds to the malicious party.

Impact:

It is presently possible to siphon any funds supplied as part of an ArbitraryExecutionPermit2Adapter::executeWithPermit or ArbitraryExecutionPermit2Adapter::executeWithBatchPermit function call due to the way these functions attempt to interact with the Permit2 contract of Uniswap.

Example:

/// @inheritdoc IArbitraryExecutionPermit2Adapter
function executeWithPermit(
  SinglePermit calldata _permit,
  AllowanceTarget[] calldata _allowanceTargets,
  ContractCall[] calldata _contractCalls,
  TransferOut[] calldata _transferOut,
  uint256 _deadline
)
  external
  payable
  checkDeadline(_deadline)
  returns (bytes[] memory _executionResults, uint256[] memory _tokenBalances)
{
  PERMIT2.takeFromCaller(_permit.token, _permit.amount, _permit.nonce, _deadline, _permit.signature);
  return _approveExecuteAndTransfer(_allowanceTargets, _contractCalls, _transferOut);
}

/// @inheritdoc IArbitraryExecutionPermit2Adapter
function executeWithBatchPermit(
  BatchPermit calldata _batchPermit,
  AllowanceTarget[] calldata _allowanceTargets,
  ContractCall[] calldata _contractCalls,
  TransferOut[] calldata _transferOut,
  uint256 _deadline
)
  external
  payable
  checkDeadline(_deadline)
  returns (bytes[] memory _executionResults, uint256[] memory _tokenBalances)
{
  PERMIT2.batchTakeFromCaller(_batchPermit.tokens, _batchPermit.nonce, _deadline, _batchPermit.signature);
  return _approveExecuteAndTransfer(_allowanceTargets, _contractCalls, _transferOut);
}

Recommendation:

We advise the code to revise its Permit2 integration approach. Similarly to the universal router of Uniswap, the Mean Finance team may opt to utilize the IPermit2::transferFrom functions that will attempt to consume an existing approval and supply the "from" argument as the msg.sender.

[Omniscia Preliminary Report] [SPA-01C] Developer Code

SPA-01C: Developer Code

Type Severity Location
Gas Optimization SwapPermit2Adapter.sol:L53-L61, L93-L101

Description:

The referenced functions should not be exposed by the contract as they relate to gas-inefficient test functionality.

Example:

/// @inheritdoc ISwapPermit2Adapter
function sellOrderSwapWithGasMeasurement(SellOrderSwapParams calldata _params)
  external
  payable
  returns (uint256 _amountIn, uint256 _amountOut, uint256 _gasSpent)
{
  uint256 _gasAtStart = gasleft();
  (_amountIn, _amountOut) = sellOrderSwap(_params);
  _gasSpent = _gasAtStart - gasleft();
}

Recommendation:

We advise them to be omitted from the production-ready version of SwapPermit2Adapter.

[Omniscia Preliminary Report] [PTS-01M] Inexistent Validation of Native Amount

PTS-01M: Inexistent Validation of Native Amount

Type Severity Location
Input Sanitization Permit2Transfers.sol:L32

Description:

The Permit2Transfers::takeFromCaller function does not validate that sufficient native funds were sent alongside the call if the _token specified is the NATIVE_TOKEN of the Token contract.

Impact:

It is possible for functions that rely on Permit2Transfers::takeFromCaller invocations to incorrectly assume they possess a msg.value equal to the _amount input when it is not the case.

As an example, the SwapPermit2Adapter::sellOrderSwap function can be invoked with an arbitrary _params.amountIn value and will yield it in its return without the msg.value necessarily equating it.

Example:

function takeFromCaller(
  IPermit2 _permit2,
  address _token,
  uint256 _amount,
  uint256 _nonce,
  uint256 _deadline,
  bytes calldata _signature
)
  internal
{
  if (address(_token) != Token.NATIVE_TOKEN) {
    _permit2.permitTransferFrom(
      // The permit message.
      IPermit2.PermitTransferFrom({
        permitted: IPermit2.TokenPermissions({ token: _token, amount: _amount }),
        nonce: _nonce,
        deadline: _deadline
      }),
      // The transfer recipient and amount.
      IPermit2.SignatureTransferDetails({ to: address(this), requestedAmount: _amount }),
      // The owner of the tokens, which must also be
      // the signer of the message, otherwise this call
      // will fail.
      msg.sender,
      // The packed signature that was the result of signing
      // the EIP712 hash of `permit`.
      _signature
    );
  }
}

Recommendation:

We advise an else branch to be introduced to the referenced if conditional, ensuring that the msg.value is equal-to the input _amount of the Permit2Transfers::takeFromCaller function.

[Omniscia Preliminary Report] [BPA-01S] Inexistent Sanitization of Input Address

BPA-01S: Inexistent Sanitization of Input Address

Type Severity Location
Input Sanitization BasePermit2Adapter.sol:L19-L21

Description:

The linked function accepts an address argument yet does not properly sanitize it.

Impact:

The presence of zero-value addresses, especially in constructor implementations, can cause the contract to be permanently inoperable. These checks are advised as zero-value inputs are a common side-effect of off-chain software related bugs.

Example:

constructor(IPermit2 _permit2) {
  PERMIT2 = _permit2;
}

Recommendation:

We advise some basic sanitization to be put in place by ensuring that the address specified is non-zero.

[Omniscia Preliminary Report] [SPA-02M] Potential Re-Entrancy Compromise

SPA-02M: Potential Re-Entrancy Compromise

Type Severity Location
Logical Fault SwapPermit2Adapter.sol:L80, L86

Description:

A SwapPermit2Adapter::buyOrderSwap interaction is re-entrant during the _amountOut distribution step prior to sending unspent funds to their intended recipient, permitting them to be compromised if _params.transferOut is different than _params.unspentTokenInRecipient.

Impact:

Should the _params.transferOut and _params.unspentTokenInRecipient variables be different, it is possible for a malicious _params.transferOut recipient to re-enter the contract and compromise the funds that were intended for the _params.unspentTokenInRecipient.

Example:

function buyOrderSwap(BuyOrderSwapParams calldata _params)
  public
  payable
  checkDeadline(_params.deadline)
  returns (uint256 _amountIn, uint256 _amountOut)
{
  // Take from caller
  PERMIT2.takeFromCaller(_params.tokenIn, _params.maxAmountIn, _params.nonce, _params.deadline, _params.signature);

  // Max approve token in
  _params.tokenIn.maxApproveIfNecessary(_params.allowanceTarget);

  // Execute swap
  _params.swapper.functionCallWithValue(_params.swapData, msg.value);

  // Distribute token out
  _amountOut = _params.tokenOut.distributeTo(_params.transferOut);

  // Check min amount
  if (_amountOut < _params.amountOut) revert ReceivedTooLittleTokenOut(_amountOut, _params.amountOut);

  // Send unspent to the set recipient
  uint256 _unspentTokenIn = _params.tokenIn.sendBalanceOnContractTo(_params.unspentTokenInRecipient);

  // Set amount in
  _amountIn = _params.maxAmountIn - _unspentTokenIn;
}

Recommendation:

We advise all function's of the SwapPermit2Adapter to be marked as non-reentrant, disallowing such vulnerabilities from being exploitable.

[Omniscia Preliminary Report] [TNE-01S] Illegible Numeric Value Representation

TNE-01S: Illegible Numeric Value Representation

Type Severity Location
Code Style Token.sol:L75

Description:

The linked representation of a numeric literal is sub-optimally represented decreasing the legibility of the codebase.

Example:

uint256 _toSend = _available * _distribution[i].shareBps / 10_000;

Recommendation:

To properly illustrate the value's purpose, we advise the following guidelines to be followed.
For values meant to depict fractions with a base of 1e18, we advise fractions to be utilized directly (i.e. 1e17 becomes 0.1e18) as they are supported.
For values meant to represent a percentage base, we advise each value to utilize the underscore (_) separator to discern the percentage decimal (i.e. 10000 becomes 100_00, 300 becomes 3_00 and so on).
Finally, for large numeric values we simply advise the underscore character to be utilized again to represent them (i.e. 1000000 becomes 1_000_000).

[Omniscia Preliminary Report] [AEP-02M] Lingering Token Approvals

AEP-02M: Lingering Token Approvals

Type Severity Location
Logical Fault ArbitraryExecutionPermit2Adapter.sol:L69

Description:

The ArbitraryExecutionPermit2Adapter contract is meant to act as an intermediary in a variety of interactions with other protocols. In the current implementation, the contract will approve the maximum amount possible to a potential call target.

As such, a non-zero approval may remain after the call. This can be exploited by scammers and other similar hijacking attacks by approving a malicious contract and attempt to inject this contract in an unsuspecting user's call-chain even if they never approved it explicitly during their invocation of the function.

Impact:

The current implementation of ArbitraryExecutionPermit2Adapter::_approveExecuteAndTransfer is not atomic, permitting it to be exploited in numerous ways including re-entrancies as well as secondary-call approval attacks.

Example:

function _approveExecuteAndTransfer(
  AllowanceTarget[] calldata _allowanceTargets,
  ContractCall[] calldata _contractCalls,
  TransferOut[] calldata _transferOut
)
  internal
  returns (bytes[] memory _executionResults, uint256[] memory _tokenBalances)
{
  // Approve targets
  for (uint256 i; i < _allowanceTargets.length;) {
    IERC20(_allowanceTargets[i].token).maxApprove(_allowanceTargets[i].allowanceTarget);
    unchecked {
      ++i;
    }
  }

  // Call contracts
  _executionResults = new bytes[](_contractCalls.length);
  for (uint256 i; i < _contractCalls.length;) {
    _executionResults[i] =
      _contractCalls[i].target.functionCallWithValue(_contractCalls[i].data, _contractCalls[i].value);
    unchecked {
      ++i;
    }
  }

  // Distribute tokens
  _tokenBalances = new uint256[](_transferOut.length);
  for (uint256 i; i < _transferOut.length;) {
    _tokenBalances[i] = _transferOut[i].token.distributeTo(_transferOut[i].distribution);
    unchecked {
      ++i;
    }
  }
}

Recommendation:

We advise all approvals performed before a function call to be zeroed out after the calls have been performed. Additionally, we advise the ArbitraryExecutionPermit2Adapter::_approveExecuteAndTransfer function to be set as non-reentrant given that each Token::distributeTo call can become re-entrant and could result in the loss of ensuing token funds.

[Omniscia Preliminary Report] [TNE-01M] Potentially Invalidated Assumption

TNE-01M: Potentially Invalidated Assumption

Type Severity Location
Mathematical Operations Token.sol:L78-L79

Description:

The Token::distributeTo function assumes that the _distribution being processed is composed of recipients that are not equal to the contract the code is being executed on, however, this is not guaranteed.

Impact:

The Token::distributeTo function performs an unchecked subtraction on the _amountLeft value that may overflow if a distribution recipient is the logic contact itself given that the balance will remain available for the next distribution.

Example:

function distributeTo(
  address _token,
  DistributionTarget[] calldata _distribution
)
  internal
  returns (uint256 _available)
{
  _available = balanceOnContract(_token);
  uint256 _amountLeft = _available;

  // Distribute amounts
  for (uint256 i; i < _distribution.length - 1;) {
    uint256 _toSend = _available * _distribution[i].shareBps / 10_000;
    sendAmountTo(_token, _toSend, _distribution[i].recipient);
    unchecked {
      // We know that _toSend <= _amountLeft because if it wasn't, sendAmountTo would have reverted
      _amountLeft -= _toSend;
      ++i;
    }
  }

  // Send amount left to the last recipient
  sendAmountTo(_token, _amountLeft, _distribution[_distribution.length - 1].recipient);
}

Recommendation:

We advise the code to perform the _amountLeft subtraction using checked arithmetics as the gas cost is minimal and protects against multiple types of exploits, including self-transfers (that can thus exceed the 10_000 BPS number and still succeed) as well as potential re-entrancy attack vectors that take advantage of that.

[Omniscia Preliminary Report] [SPA-03M] Race-Condition of Permit2 Approvals

SPA-03M: Race-Condition of Permit2 Approvals

Type Severity Location
Logical Fault SwapPermit2Adapter.sol:L27, L34, L64, L71

Description:

The SwapPermit2Adapter will integrate with the Permit2 contract in a non-standard way, consuming a direct permission of a transfer-from operation.

The contract is susceptible to on-chain conditions that would result in the compromise of a user's permitted funds.

To elaborate, the IPermit2::permitTransferFrom function can be invoked by an arbitrary party. As such, a SwapPermit2Adapter::buyOrderSwap function call can be detected by off-chain bots and its permit consumed by a direct call to the Permit2 contract.

As the SwapPermit2Adapter permits arbitrary executions due to the swapper and swapData not being validated, the consumption of the permit can be coupled with a call to the contract that siphons those funds to the malicious party.

Impact:

It is presently possible to siphon any funds supplied as part of a SwapPermit2Adapter::sellOrderSwap or SwapPermit2Adapter::buyOrderSwap function call due to the way these functions attempt to interact with the Permit2 contract of Uniswap.

Example:

/// @inheritdoc ISwapPermit2Adapter
function sellOrderSwap(SellOrderSwapParams calldata _params)
  public
  payable
  checkDeadline(_params.deadline)
  returns (uint256 _amountIn, uint256 _amountOut)
{
  // Take from caller
  PERMIT2.takeFromCaller(_params.tokenIn, _params.amountIn, _params.nonce, _params.deadline, _params.signature);

  // Max approve token in
  _params.tokenIn.maxApproveIfNecessary(_params.allowanceTarget);

  // Execute swap
  _params.swapper.functionCallWithValue(_params.swapData, msg.value);

  // Distribute token out
  _amountOut = _params.tokenOut.distributeTo(_params.transferOut);

  // Check min amount
  if (_amountOut < _params.minAmountOut) revert ReceivedTooLittleTokenOut(_amountOut, _params.minAmountOut);

  // Set amount in
  _amountIn = _params.amountIn;
}

Recommendation:

We advise the code to revise its Permit2 integration approach. Similarly to the universal router of Uniswap, the Mean Finance team may opt to utilize the IPermit2::transferFrom functions that will attempt to consume an existing approval and supply the "from" argument as the msg.sender.

[Omniscia Preliminary Report] [PTS-02M] Inexistent Compatibility of Native Funds

PTS-02M: Inexistent Compatibility of Native Funds

Type Severity Location
Logical Fault Permit2Transfers.sol:L61-L85

Description:

The Permit2Transfers::batchTakeFromCaller function is incompatible with the NATIVE_TOKEN as it will attempt to acquire it from the Permit2 instance instead of validating the call's msg.value.

Impact:

Based on the implementation of ArbitraryExecutionPermit2Adapter::executeWithBatchPermit, which is set as payable, the Permit2Transfers::batchTakeFromCaller is expected to be compatible with native funds. This is not the case in the current implementation by Permit2Transfers.

Example:

function batchTakeFromCaller(
  IPermit2 _permit2,
  IPermit2.TokenPermissions[] calldata _tokens,
  uint256 _nonce,
  uint256 _deadline,
  bytes calldata _signature
)
  internal
{
  if (_tokens.length > 0) {
    _permit2.permitTransferFrom(
      // The permit message.
      IPermit2.PermitBatchTransferFrom({ permitted: _tokens, nonce: _nonce, deadline: _deadline }),
      // The transfer recipients and amounts.
      _buildTransferDetails(_tokens),
      // The owner of the tokens, which must also be
      // the signer of the message, otherwise this call
      // will fail.
      msg.sender,
      // The packed signature that was the result of signing
      // the EIP712 hash of `permit`.
      _signature
    );
  }
}

Recommendation:

We advise the code to properly omit the NATIVE_TOKEN from the _tokens array in the PermitBatchTransferFrom struct as well as to skip it when building the transfer details via Permit2Transfers::_buildTransferDetails.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.