Skip to content

Commit

Permalink
Fix DLLR EIP-2612 bug with Permit2
Browse files Browse the repository at this point in the history
* introduce: permit2

* update nonce mechanism

* add permit2 deployment file

* add SIP args & onchain test

* add unit test for signature exploit pervention

* consider new function to implement permit2

* add MocIntegration deployment file testnet

* remove nonce in the permit2 integrated contract

* add testnet deployment files

* add mainnet deployment for MocIntegration

* .prettierignore permit2 folder contracts

* update dist
  • Loading branch information
tjcloa authored Feb 8, 2024
1 parent 4495749 commit e2ddb95
Show file tree
Hide file tree
Showing 72 changed files with 5,219 additions and 87 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ tmp
temp
coverage*
gasReporterOutput.json
contracts/permit2
74 changes: 72 additions & 2 deletions contracts/integration/MoC/MocIntegration.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol";
import { IMocMintRedeemDoc } from "./IMoC.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "../../meta-asset-token/DLLR.sol";
import "../../interfaces/IMassetManager.sol";
import { IDLLR, PermitParams } from "../../interfaces/IDLLR.sol";
import { IPermit2, ISignatureTransfer } from "../../permit2/interfaces/IPermit2.sol";

/// @notice This contract provides compound functions with Money On Chain wrapping them in one transaction for convenience and to save on gas

Check warning on line 14 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Line length must be no more than 140 but current length is 141

Check warning on line 14 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Line length must be no more than 140 but current length is 141

Check warning on line 14 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Line length must be no more than 140 but current length is 141
contract MocIntegration is OwnableUpgradeable, ERC1967UpgradeUpgradeable {
using Counters for Counters.Counter;
// Money On Chain DoC redeem interface at MoC main contract address
IMocMintRedeemDoc public immutable moc;
// IERC20@[DoC token]
Expand All @@ -20,6 +23,8 @@ contract MocIntegration is OwnableUpgradeable, ERC1967UpgradeUpgradeable {

address public mocVendorAccount;

IPermit2 public immutable permit2;

event GetDocFromDllrAndRedeemRBTC(address indexed from, uint256 fromDLLR, uint256 toRBTC);
event MocVendorAccountSet(address newMocVendorAccount);

Expand All @@ -29,18 +34,26 @@ contract MocIntegration is OwnableUpgradeable, ERC1967UpgradeUpgradeable {
* @param _dllr DLLR contract address
* @param _massetManager MassetManager contract address
*/
constructor(address _moc, address _doc, address _dllr, address _massetManager) {
constructor(
address _moc,
address _doc,
address _dllr,
address _massetManager,
address _permit2
) {

Check warning on line 43 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark visibility in function

Check warning on line 43 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark visibility in function

Check warning on line 43 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark visibility in function
require(
_moc != address(0) &&
_doc != address(0) &&
_dllr != address(0) &&
_massetManager != address(0),
_massetManager != address(0) &&
_permit2 != address(0),
"MocIntegration:: no null addresses allowed"
);
moc = IMocMintRedeemDoc(_moc);

Check warning on line 52 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted

Check warning on line 52 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted

Check warning on line 52 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted
doc = IERC20(_doc);

Check warning on line 53 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted

Check warning on line 53 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted

Check warning on line 53 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted
dllr = IDLLR(_dllr);

Check warning on line 54 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted

Check warning on line 54 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted

Check warning on line 54 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted
massetManager = IMassetManager(_massetManager);

Check warning on line 55 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted

Check warning on line 55 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted

Check warning on line 55 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted
permit2 = IPermit2(_permit2);

Check warning on line 56 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted

Check warning on line 56 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted

Check warning on line 56 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Explicitly mark all external contracts as trusted or untrusted
}

function initialize(address payable _mocVendorAccount) external initializer {
Expand Down Expand Up @@ -98,6 +111,45 @@ contract MocIntegration is OwnableUpgradeable, ERC1967UpgradeUpgradeable {
emit GetDocFromDllrAndRedeemRBTC(msg.sender, _dllrAmount, rbtcAmount);
}

/**
* @notice how getDocFromDllrAndRedeemRBTC function works:
* -------------------------------------------------------------------------------------------
* | Mynt | Money On Chain |
* -------------------------------------------------------------------------------------------
* | get DLLR (EIP-2612) -> convert DLLR to DoC | -> get RBTC from DoC -> send RBTC to user |
* -------------------------------------------------------------------------------------------
*
* @param permit permit data, in form of PermitTransferFrom struct.
* @param signature of the permit data.
*/
function getDocFromDllrAndRedeemRbtcWithPermit2(
ISignatureTransfer.PermitTransferFrom memory permit,
bytes memory signature
) external {
address thisAddress = address(this);
uint256 _dllrAmount = permit.permitted.amount;

ISignatureTransfer.SignatureTransferDetails
memory transferDetails = _generateTransferDetails(thisAddress, _dllrAmount);

permit2.permitTransferFrom(permit, transferDetails, msg.sender, signature);

// redeem DoC from DLLR
require(
massetManager.redeemTo(address(doc), _dllrAmount, thisAddress) == _dllrAmount,
"MocIntegration:: redeemed incorrect DoC amount"
);

// redeem RBTC from DoC using Money On Chain and send to the user
uint256 rbtcBalanceBefore = thisAddress.balance;
moc.redeemFreeDocVendors(_dllrAmount, payable(mocVendorAccount));
uint256 rbtcAmount = thisAddress.balance - rbtcBalanceBefore;
(bool success, ) = msg.sender.call{ value: rbtcAmount }("");
require(success, "MocIntegration:: error transferring redeemed RBTC");

emit GetDocFromDllrAndRedeemRBTC(msg.sender, _dllrAmount, rbtcAmount);
}

/// Set MoC registered Vendor account to receive markup fees https://docs.moneyonchain.com/main-rbtc-contract/integration-with-moc-platform/vendors

Check warning on line 153 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Line length must be no more than 140 but current length is 151

Check warning on line 153 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Line length must be no more than 140 but current length is 151

Check warning on line 153 in contracts/integration/MoC/MocIntegration.sol

View workflow job for this annotation

GitHub Actions / build

Line length must be no more than 140 but current length is 151
function setMocVendorAccount(address payable newMocVedorAccount) external onlyOwner {
_setMocVendorAccount(newMocVedorAccount);
Expand All @@ -116,4 +168,22 @@ contract MocIntegration is OwnableUpgradeable, ERC1967UpgradeUpgradeable {
function getProxyImplementation() external view returns (address) {
return ERC1967UpgradeUpgradeable._getImplementation();
}

/**
* @dev view function to construct SignatureTransferDetails struct to be used by Permit2
*
* @param _to ultimate recipient
* @param _amount amount of transfer
*
* @return SignatureTransferDetails struct object
*/
function _generateTransferDetails(
address _to,
uint256 _amount
) private pure returns (ISignatureTransfer.SignatureTransferDetails memory) {
ISignatureTransfer.SignatureTransferDetails memory transferDetails = ISignatureTransfer
.SignatureTransferDetails({ to: _to, requestedAmount: _amount });

return transferDetails;
}
}
162 changes: 162 additions & 0 deletions contracts/permit2/AllowanceTransfer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import { ERC20 } from "./ERC20.sol";
import { SafeTransferLib } from "./libraries/SafeTransferLib.sol";
import { PermitHash } from "./libraries/PermitHash.sol";
import { SignatureVerification } from "./libraries/SignatureVerification.sol";
import { EIP712 } from "./EIP712.sol";
import { IAllowanceTransfer } from "./interfaces/IAllowanceTransfer.sol";
import { SignatureExpired, InvalidNonce } from "./PermitErrors.sol";
import { Allowance } from "./libraries/Allowance.sol";

contract AllowanceTransfer is IAllowanceTransfer, EIP712 {
using SignatureVerification for bytes;
using SafeTransferLib for ERC20;
using PermitHash for PermitSingle;
using PermitHash for PermitBatch;
using Allowance for PackedAllowance;

/// @notice Maps users to tokens to spender addresses and information about the approval on the token
/// @dev Indexed in the order of token owner address, token address, spender address
/// @dev The stored word saves the allowed amount, expiration on the allowance, and nonce
mapping(address => mapping(address => mapping(address => PackedAllowance))) public allowance;

/// @inheritdoc IAllowanceTransfer
function approve(address token, address spender, uint160 amount, uint48 expiration) external {
PackedAllowance storage allowed = allowance[msg.sender][token][spender];
allowed.updateAmountAndExpiration(amount, expiration);
emit Approval(msg.sender, token, spender, amount, expiration);
}

/// @inheritdoc IAllowanceTransfer
function permit(
address owner,
PermitSingle memory permitSingle,
bytes calldata signature
) external {
if (block.timestamp > permitSingle.sigDeadline)
revert SignatureExpired(permitSingle.sigDeadline);

// Verify the signer address from the signature.
signature.verify(_hashTypedData(permitSingle.hash()), owner);

_updateApproval(permitSingle.details, owner, permitSingle.spender);
}

/// @inheritdoc IAllowanceTransfer
function permit(
address owner,
PermitBatch memory permitBatch,
bytes calldata signature
) external {
if (block.timestamp > permitBatch.sigDeadline)
revert SignatureExpired(permitBatch.sigDeadline);

// Verify the signer address from the signature.
signature.verify(_hashTypedData(permitBatch.hash()), owner);

address spender = permitBatch.spender;
unchecked {
uint256 length = permitBatch.details.length;
for (uint256 i = 0; i < length; ++i) {
_updateApproval(permitBatch.details[i], owner, spender);
}
}
}

/// @inheritdoc IAllowanceTransfer
function transferFrom(address from, address to, uint160 amount, address token) external {
_transfer(from, to, amount, token);
}

/// @inheritdoc IAllowanceTransfer
function transferFrom(AllowanceTransferDetails[] calldata transferDetails) external {
unchecked {
uint256 length = transferDetails.length;
for (uint256 i = 0; i < length; ++i) {
AllowanceTransferDetails memory transferDetail = transferDetails[i];
_transfer(
transferDetail.from,
transferDetail.to,
transferDetail.amount,
transferDetail.token
);
}
}
}

/// @notice Internal function for transferring tokens using stored allowances
/// @dev Will fail if the allowed timeframe has passed
function _transfer(address from, address to, uint160 amount, address token) private {
PackedAllowance storage allowed = allowance[from][token][msg.sender];

if (block.timestamp > allowed.expiration) revert AllowanceExpired(allowed.expiration);

uint256 maxAmount = allowed.amount;
if (maxAmount != type(uint160).max) {
if (amount > maxAmount) {
revert InsufficientAllowance(maxAmount);
} else {
unchecked {
allowed.amount = uint160(maxAmount) - amount;
}
}
}

// Transfer the tokens from the from address to the recipient.
ERC20(token).safeTransferFrom(from, to, amount);
}

/// @inheritdoc IAllowanceTransfer
function lockdown(TokenSpenderPair[] calldata approvals) external {
address owner = msg.sender;
// Revoke allowances for each pair of spenders and tokens.
unchecked {
uint256 length = approvals.length;
for (uint256 i = 0; i < length; ++i) {
address token = approvals[i].token;
address spender = approvals[i].spender;

allowance[owner][token][spender].amount = 0;
emit Lockdown(owner, token, spender);
}
}
}

/// @inheritdoc IAllowanceTransfer
function invalidateNonces(address token, address spender, uint48 newNonce) external {
uint48 oldNonce = allowance[msg.sender][token][spender].nonce;

if (newNonce <= oldNonce) revert InvalidNonce();

// Limit the amount of nonces that can be invalidated in one transaction.
unchecked {
uint48 delta = newNonce - oldNonce;
if (delta > type(uint16).max) revert ExcessiveInvalidation();
}

allowance[msg.sender][token][spender].nonce = newNonce;
emit NonceInvalidation(msg.sender, token, spender, newNonce, oldNonce);
}

/// @notice Sets the new values for amount, expiration, and nonce.
/// @dev Will check that the signed nonce is equal to the current nonce and then incrememnt the nonce value by 1.
/// @dev Emits a Permit event.
function _updateApproval(
PermitDetails memory details,
address owner,
address spender
) private {
uint48 nonce = details.nonce;
address token = details.token;
uint160 amount = details.amount;
uint48 expiration = details.expiration;
PackedAllowance storage allowed = allowance[owner][token][spender];

if (allowed.nonce != nonce) revert InvalidNonce();

allowed.updateAll(amount, expiration, nonce);
emit Permit(owner, token, spender, amount, expiration, nonce);
}
}
45 changes: 45 additions & 0 deletions contracts/permit2/EIP712.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import { IEIP712 } from "./interfaces/IEIP712.sol";

/// @notice EIP712 helpers for permit2
/// @dev Maintains cross-chain replay protection in the event of a fork
/// @dev Reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/EIP712.sol
contract EIP712 is IEIP712 {
// Cache the domain separator as an immutable value, but also store the chain id that it
// corresponds to, in order to invalidate the cached domain separator if the chain id changes.
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
uint256 private immutable _CACHED_CHAIN_ID;

bytes32 private constant _HASHED_NAME = keccak256("Permit2");
bytes32 private constant _TYPE_HASH =
keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)");

constructor() {
_CACHED_CHAIN_ID = block.chainid;
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME);
}

/// @notice Returns the domain separator for the current chain.
/// @dev Uses cached version if chainid and address are unchanged from construction.
function DOMAIN_SEPARATOR() public view override returns (bytes32) {
return
block.chainid == _CACHED_CHAIN_ID
? _CACHED_DOMAIN_SEPARATOR
: _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME);
}

/// @notice Builds a domain separator using the current chainId and contract address.
function _buildDomainSeparator(
bytes32 typeHash,
bytes32 nameHash
) private view returns (bytes32) {
return keccak256(abi.encode(typeHash, nameHash, block.chainid, address(this)));
}

/// @notice Creates an EIP-712 typed data hash
function _hashTypedData(bytes32 dataHash) internal view returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), dataHash));
}
}
Loading

0 comments on commit e2ddb95

Please sign in to comment.