Skip to content

Commit

Permalink
Merge pull request #80 from morpho-labs/feat/permit
Browse files Browse the repository at this point in the history
Feat permit
  • Loading branch information
MerlinEgalite authored Sep 20, 2023
2 parents c655c8c + 9c26dc3 commit 78af18a
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 2 deletions.
8 changes: 7 additions & 1 deletion src/MetaMorpho.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ import {
Math,
SafeERC20
} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {IERC20Metadata, ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";

contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {
contract MetaMorpho is ERC4626, ERC20Permit, Ownable2Step, IMetaMorpho {
using Math for uint256;
using UtilsLib for uint256;
using SafeCast for uint256;
Expand Down Expand Up @@ -71,6 +72,7 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {

constructor(address morpho, uint256 initialTimelock, address _asset, string memory _name, string memory _symbol)
ERC4626(IERC20(_asset))
ERC20Permit(_name)
ERC20(_name, _symbol)
{
require(initialTimelock <= MAX_TIMELOCK, ErrorsLib.MAX_TIMELOCK_EXCEEDED);
Expand Down Expand Up @@ -258,6 +260,10 @@ contract MetaMorpho is ERC4626, Ownable2Step, IMetaMorpho {

/* ERC4626 (PUBLIC) */

function decimals() public view override(IERC20Metadata, ERC20, ERC4626) returns (uint8) {
return ERC4626.decimals();
}

function maxWithdraw(address owner) public view override(IERC4626, ERC4626) returns (uint256 assets) {
(assets,) = _maxWithdraw(owner);
}
Expand Down
168 changes: 168 additions & 0 deletions test/forge/PermitTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import "./helpers/SigUtils.sol";
import "./helpers/BaseTest.sol";

contract PermitTest is BaseTest {
SigUtils internal sigUtils;

uint256 internal constant OWNER_PK = 0xA11CE;
uint256 internal constant SPENDER_PK = 0xB0B;

address internal owner;
address internal spender;

function setUp() public override {
super.setUp();

sigUtils = new SigUtils(vault.DOMAIN_SEPARATOR());

owner = vm.addr(OWNER_PK);
spender = vm.addr(SPENDER_PK);

deal(address(vault), owner, 1e18);
}

function testPermit() public {
Permit memory permit = Permit({owner: owner, spender: spender, value: 1e18, nonce: 0, deadline: 1 days});

bytes32 digest = sigUtils.getTypedDataHash(permit);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_PK, digest);

vault.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);

assertEq(vault.allowance(owner, spender), 1e18);
assertEq(vault.nonces(owner), 1);
}

function testRevertExpiredPermit() public {
Permit memory permit =
Permit({owner: owner, spender: spender, value: 1e18, nonce: vault.nonces(owner), deadline: 1 days});

bytes32 digest = sigUtils.getTypedDataHash(permit);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_PK, digest);

vm.warp(1 days + 1 seconds); // fast forward one second past the deadline

vm.expectRevert("ERC20Permit: expired deadline");
vault.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
}

function testRevertInvalidSigner() public {
Permit memory permit =
Permit({owner: owner, spender: spender, value: 1e18, nonce: vault.nonces(owner), deadline: 1 days});

bytes32 digest = sigUtils.getTypedDataHash(permit);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(SPENDER_PK, digest); // spender signs owner's approval

vm.expectRevert("ERC20Permit: invalid signature");
vault.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
}

function testRevertInvalidNonce() public {
Permit memory permit = Permit({
owner: owner,
spender: spender,
value: 1e18,
nonce: 1, // owner nonce stored on-chain is 0
deadline: 1 days
});

bytes32 digest = sigUtils.getTypedDataHash(permit);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_PK, digest);

vm.expectRevert("ERC20Permit: invalid signature");
vault.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
}

function testRevertSignatureReplay() public {
Permit memory permit = Permit({owner: owner, spender: spender, value: 1e18, nonce: 0, deadline: 1 days});

bytes32 digest = sigUtils.getTypedDataHash(permit);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_PK, digest);

vault.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);

vm.expectRevert("ERC20Permit: invalid signature");
vault.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
}

function testTransferFromLimitedPermit() public {
Permit memory permit = Permit({owner: owner, spender: spender, value: 1e18, nonce: 0, deadline: 1 days});

bytes32 digest = sigUtils.getTypedDataHash(permit);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_PK, digest);

vault.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);

vm.prank(spender);
vault.transferFrom(owner, spender, 1e18);

assertEq(vault.balanceOf(owner), 0);
assertEq(vault.balanceOf(spender), 1e18);
assertEq(vault.allowance(owner, spender), 0);
}

function testTransferFromMaxPermit() public {
Permit memory permit =
Permit({owner: owner, spender: spender, value: type(uint256).max, nonce: 0, deadline: 1 days});

bytes32 digest = sigUtils.getTypedDataHash(permit);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_PK, digest);

vault.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);

vm.prank(spender);
vault.transferFrom(owner, spender, 1e18);

assertEq(vault.balanceOf(owner), 0);
assertEq(vault.balanceOf(spender), 1e18);
assertEq(vault.allowance(owner, spender), type(uint256).max);
}

function testFailInvalidAllowance() public {
Permit memory permit = Permit({
owner: owner,
spender: spender,
value: 5e17, // approve only 0.5 tokens
nonce: 0,
deadline: 1 days
});

bytes32 digest = sigUtils.getTypedDataHash(permit);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_PK, digest);

vault.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);

vm.prank(spender);
vault.transferFrom(owner, spender, 1e18); // attempt to transfer 1 vault
}

function testFailInvalidBalance() public {
Permit memory permit = Permit({
owner: owner,
spender: spender,
value: 2e18, // approve 2 tokens
nonce: 0,
deadline: 1 days
});

bytes32 digest = sigUtils.getTypedDataHash(permit);

(uint8 v, bytes32 r, bytes32 s) = vm.sign(OWNER_PK, digest);

vault.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);

vm.prank(spender);
vault.transferFrom(owner, spender, 2e18); // attempt to transfer 2 tokens (owner only owns 1)
}
}
1 change: 0 additions & 1 deletion test/forge/helpers/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
pragma solidity ^0.8.0;

import "@morpho-blue/interfaces/IMorpho.sol";
import {IOracle} from "@morpho-blue/interfaces/IOracle.sol";

import {MarketParamsLib} from "@morpho-blue/libraries/MarketParamsLib.sol";
import {MorphoLib} from "@morpho-blue/libraries/periphery/MorphoLib.sol";
Expand Down
33 changes: 33 additions & 0 deletions test/forge/helpers/SigUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

struct Permit {
address owner;
address spender;
uint256 value;
uint256 nonce;
uint256 deadline;
}

// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;

contract SigUtils {
bytes32 internal DOMAIN_SEPARATOR;

constructor(bytes32 _DOMAIN_SEPARATOR) {
DOMAIN_SEPARATOR = _DOMAIN_SEPARATOR;
}

// computes the hash of a permit
function getStructHash(Permit memory _permit) internal pure returns (bytes32) {
return keccak256(
abi.encode(PERMIT_TYPEHASH, _permit.owner, _permit.spender, _permit.value, _permit.nonce, _permit.deadline)
);
}

// computes the hash of the fully encoded EIP-712 message for the domain, which can be used to recover the signer
function getTypedDataHash(Permit memory _permit) public view returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, getStructHash(_permit)));
}
}

0 comments on commit 78af18a

Please sign in to comment.