Skip to content

Commit

Permalink
Merge pull request #114 from morpho-labs/feat/approval-with-sig
Browse files Browse the repository at this point in the history
Add authorization with signature
  • Loading branch information
MathisGD authored Jul 31, 2023
2 parents 60278e4 + 588a852 commit 152087d
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 42 deletions.
63 changes: 53 additions & 10 deletions src/Blue.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,30 @@ import {SafeTransferLib} from "src/libraries/SafeTransferLib.sol";
uint256 constant MAX_FEE = 0.25e18;
uint256 constant ALPHA = 0.5e18;

/// @dev The EIP-712 typeHash for EIP712Domain.
bytes32 constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)");

/// @dev The EIP-712 typeHash for Authorization.
bytes32 constant AUTHORIZATION_TYPEHASH =
keccak256("Authorization(address authorizer,address authorized,bool isAuthorized,uint256 nonce,uint256 deadline)");

/// @notice Contains the `v`, `r` and `s` parameters of an ECDSA signature.
struct Signature {
uint8 v;
bytes32 r;
bytes32 s;
}

contract Blue is IFlashLender {
using SharesMath for uint256;
using FixedPointMathLib for uint256;
using SafeTransferLib for IERC20;
using MarketLib for Market;

// Immutables.

bytes32 public immutable DOMAIN_SEPARATOR;

// Storage.

// Owner.
Expand Down Expand Up @@ -55,13 +73,17 @@ contract Blue is IFlashLender {
mapping(IIrm => bool) public isIrmEnabled;
// Enabled LLTVs.
mapping(uint256 => bool) public isLltvEnabled;
// User's managers.
mapping(address => mapping(address => bool)) public isApproved;
// User's authorizations. Note that by default, msg.sender is authorized by themself.
mapping(address => mapping(address => bool)) public isAuthorized;
// User's nonces. Used to prevent replay attacks with EIP-712 signatures.
mapping(address => uint256) public nonce;

// Constructor.

constructor(address newOwner) {
owner = newOwner;

DOMAIN_SEPARATOR = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256("Blue"), block.chainid, address(this)));
}

// Modifiers.
Expand Down Expand Up @@ -133,7 +155,7 @@ contract Blue is IFlashLender {
Id id = market.id();
require(lastUpdate[id] != 0, Errors.MARKET_NOT_CREATED);
require(amount != 0, Errors.ZERO_AMOUNT);
require(_isSenderOrIsApproved(onBehalf), Errors.MANAGER_NOT_APPROVED);
require(_isSenderAuthorized(onBehalf), Errors.UNAUTHORIZED);

_accrueInterests(market, id);

Expand All @@ -154,7 +176,7 @@ contract Blue is IFlashLender {
Id id = market.id();
require(lastUpdate[id] != 0, Errors.MARKET_NOT_CREATED);
require(amount != 0, Errors.ZERO_AMOUNT);
require(_isSenderOrIsApproved(onBehalf), Errors.MANAGER_NOT_APPROVED);
require(_isSenderAuthorized(onBehalf), Errors.UNAUTHORIZED);

_accrueInterests(market, id);

Expand Down Expand Up @@ -211,7 +233,7 @@ contract Blue is IFlashLender {
Id id = market.id();
require(lastUpdate[id] != 0, Errors.MARKET_NOT_CREATED);
require(amount != 0, Errors.ZERO_AMOUNT);
require(_isSenderOrIsApproved(onBehalf), Errors.MANAGER_NOT_APPROVED);
require(_isSenderAuthorized(onBehalf), Errors.UNAUTHORIZED);

_accrueInterests(market, id);

Expand Down Expand Up @@ -275,14 +297,35 @@ contract Blue is IFlashLender {
IERC20(token).safeTransferFrom(address(receiver), address(this), amount);
}

// Position management.
// Authorizations.

/// @dev The signature is malleable, but it has no impact on the security here.
function setAuthorization(
address authorizer,
address authorized,
bool newIsAuthorized,
uint256 deadline,
Signature calldata signature
) external {
require(block.timestamp < deadline, Errors.SIGNATURE_EXPIRED);

bytes32 hashStruct = keccak256(
abi.encode(AUTHORIZATION_TYPEHASH, authorizer, authorized, newIsAuthorized, nonce[authorizer]++, deadline)
);
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, hashStruct));
address signatory = ecrecover(digest, signature.v, signature.r, signature.s);

require(signatory != address(0) && authorizer == signatory, Errors.INVALID_SIGNATURE);

isAuthorized[signatory][authorized] = newIsAuthorized;
}

function setApproval(address manager, bool isAllowed) external {
isApproved[msg.sender][manager] = isAllowed;
function setAuthorization(address authorized, bool newIsAuthorized) external {
isAuthorized[msg.sender][authorized] = newIsAuthorized;
}

function _isSenderOrIsApproved(address user) internal view returns (bool) {
return msg.sender == user || isApproved[user][msg.sender];
function _isSenderAuthorized(address user) internal view returns (bool) {
return msg.sender == user || isAuthorized[user][msg.sender];
}

// Interests management.
Expand Down
12 changes: 7 additions & 5 deletions src/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,27 @@ library Errors {

string internal constant LLTV_TOO_HIGH = "LLTV too high";

string internal constant MAX_FEE_EXCEEDED = "fee must be <= MAX_FEE";
string internal constant MAX_FEE_EXCEEDED = "MAX_FEE exceeded";

string internal constant IRM_NOT_ENABLED = "IRM not enabled";

string internal constant LLTV_NOT_ENABLED = "LLTV not enabled";

string internal constant MARKET_CREATED = "market already exists";
string internal constant MARKET_CREATED = "market created";

string internal constant MARKET_NOT_CREATED = "unknown market";
string internal constant MARKET_NOT_CREATED = "market not created";

string internal constant ZERO_AMOUNT = "zero amount";

string internal constant MANAGER_NOT_APPROVED = "not approved";
string internal constant UNAUTHORIZED = "unauthorized";

string internal constant INSUFFICIENT_COLLATERAL = "insufficient collateral";

string internal constant INSUFFICIENT_LIQUIDITY = "insufficient liquidity";

string internal constant HEALTHY_POSITION = "position is healthy";

string internal constant INVALID_SUCCESS_HASH = "invalid success hash";
string internal constant INVALID_SIGNATURE = "invalid signature";

string internal constant SIGNATURE_EXPIRED = "signature expired";
}
84 changes: 57 additions & 27 deletions test/forge/Blue.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ pragma solidity 0.8.21;
import "forge-std/Test.sol";
import "forge-std/console.sol";

import {SigUtils} from "./helpers/SigUtils.sol";

import "src/Blue.sol";
import {
IBlueLiquidateCallback,
Expand Down Expand Up @@ -228,15 +230,15 @@ contract BlueTest is
fee = bound(fee, 0, FixedPointMathLib.WAD);

vm.prank(OWNER);
vm.expectRevert("unknown market");
vm.expectRevert(bytes(Errors.MARKET_NOT_CREATED));
blue.setFee(marketFuzz, fee);
}

function testSetFeeShouldRevertIfNotOwner(uint256 fee, address caller) public {
vm.assume(caller != OWNER);
fee = bound(fee, 0, FixedPointMathLib.WAD);

vm.expectRevert("not owner");
vm.expectRevert(bytes(Errors.NOT_OWNER));
blue.setFee(market, fee);
}

Expand All @@ -250,7 +252,7 @@ contract BlueTest is
function testSetFeeRecipientShouldRevertIfNotOwner(address caller, address recipient) public {
vm.assume(caller != OWNER);

vm.expectRevert("not owner");
vm.expectRevert(bytes(Errors.NOT_OWNER));
vm.prank(caller);
blue.setFeeRecipient(recipient);
}
Expand Down Expand Up @@ -609,48 +611,48 @@ contract BlueTest is
function testUnknownMarket(Market memory marketFuzz) public {
vm.assume(neq(marketFuzz, market));

vm.expectRevert("unknown market");
vm.expectRevert(bytes(Errors.MARKET_NOT_CREATED));
blue.supply(marketFuzz, 1, address(this), hex"");

vm.expectRevert("unknown market");
vm.expectRevert(bytes(Errors.MARKET_NOT_CREATED));
blue.withdraw(marketFuzz, 1, address(this), address(this));

vm.expectRevert("unknown market");
vm.expectRevert(bytes(Errors.MARKET_NOT_CREATED));
blue.borrow(marketFuzz, 1, address(this), address(this));

vm.expectRevert("unknown market");
vm.expectRevert(bytes(Errors.MARKET_NOT_CREATED));
blue.repay(marketFuzz, 1, address(this), hex"");

vm.expectRevert("unknown market");
vm.expectRevert(bytes(Errors.MARKET_NOT_CREATED));
blue.supplyCollateral(marketFuzz, 1, address(this), hex"");

vm.expectRevert("unknown market");
vm.expectRevert(bytes(Errors.MARKET_NOT_CREATED));
blue.withdrawCollateral(marketFuzz, 1, address(this), address(this));

vm.expectRevert("unknown market");
vm.expectRevert(bytes(Errors.MARKET_NOT_CREATED));
blue.liquidate(marketFuzz, address(0), 1, hex"");
}

function testAmountZero() public {
vm.expectRevert("zero amount");
vm.expectRevert(bytes(Errors.ZERO_AMOUNT));
blue.supply(market, 0, address(this), hex"");

vm.expectRevert("zero amount");
vm.expectRevert(bytes(Errors.ZERO_AMOUNT));
blue.withdraw(market, 0, address(this), address(this));

vm.expectRevert("zero amount");
vm.expectRevert(bytes(Errors.ZERO_AMOUNT));
blue.borrow(market, 0, address(this), address(this));

vm.expectRevert("zero amount");
vm.expectRevert(bytes(Errors.ZERO_AMOUNT));
blue.repay(market, 0, address(this), hex"");

vm.expectRevert("zero amount");
vm.expectRevert(bytes(Errors.ZERO_AMOUNT));
blue.supplyCollateral(market, 0, address(this), hex"");

vm.expectRevert("zero amount");
vm.expectRevert(bytes(Errors.ZERO_AMOUNT));
blue.withdrawCollateral(market, 0, address(this), address(this));

vm.expectRevert("zero amount");
vm.expectRevert(bytes(Errors.ZERO_AMOUNT));
blue.liquidate(market, address(0), 0, hex"");
}

Expand All @@ -667,36 +669,36 @@ contract BlueTest is
blue.withdrawCollateral(market, amount, address(this), address(this));
}

function testSetApproval(address manager, bool isAllowed) public {
blue.setApproval(manager, isAllowed);
assertEq(blue.isApproved(address(this), manager), isAllowed);
function testSetAuthorization(address authorized, bool isAuthorized) public {
blue.setAuthorization(authorized, isAuthorized);
assertEq(blue.isAuthorized(address(this), authorized), isAuthorized);
}

function testNotApproved(address attacker) public {
function testNotAuthorized(address attacker) public {
vm.assume(attacker != address(this));

vm.startPrank(attacker);

vm.expectRevert("not approved");
vm.expectRevert(bytes(Errors.UNAUTHORIZED));
blue.withdraw(market, 1, address(this), address(this));
vm.expectRevert("not approved");
vm.expectRevert(bytes(Errors.UNAUTHORIZED));
blue.withdrawCollateral(market, 1, address(this), address(this));
vm.expectRevert("not approved");
vm.expectRevert(bytes(Errors.UNAUTHORIZED));
blue.borrow(market, 1, address(this), address(this));

vm.stopPrank();
}

function testApproved(address manager) public {
function testAuthorization(address authorized) public {
borrowableAsset.setBalance(address(this), 100 ether);
collateralAsset.setBalance(address(this), 100 ether);

blue.supply(market, 100 ether, address(this), hex"");
blue.supplyCollateral(market, 100 ether, address(this), hex"");

blue.setApproval(manager, true);
blue.setAuthorization(authorized, true);

vm.startPrank(manager);
vm.startPrank(authorized);

blue.withdraw(market, 1 ether, address(this), address(this));
blue.withdrawCollateral(market, 1 ether, address(this), address(this));
Expand All @@ -705,6 +707,34 @@ contract BlueTest is
vm.stopPrank();
}

function testAuthorizationWithSig(uint128 deadline, address authorized, uint256 privateKey, bool isAuthorized)
public
{
vm.assume(deadline > block.timestamp);
privateKey = bound(privateKey, 1, type(uint32).max); // "Private key must be less than the secp256k1 curve order (115792089237316195423570985008687907852837564279074904382605163141518161494337)."
address authorizer = vm.addr(privateKey);

SigUtils.Authorization memory authorization = SigUtils.Authorization({
authorizer: authorizer,
authorized: authorized,
isAuthorized: isAuthorized,
nonce: blue.nonce(authorizer),
deadline: block.timestamp + deadline
});

bytes32 digest = SigUtils.getTypedDataHash(blue.DOMAIN_SEPARATOR(), authorization);

Signature memory sig;
(sig.v, sig.r, sig.s) = vm.sign(privateKey, digest);

blue.setAuthorization(
authorization.authorizer, authorization.authorized, authorization.isAuthorized, authorization.deadline, sig
);

assertEq(blue.isAuthorized(authorizer, authorized), isAuthorized);
assertEq(blue.nonce(authorizer), 1);
}

function testFlashLoan(uint256 amount) public {
amount = bound(amount, 1, 2 ** 64);

Expand Down
36 changes: 36 additions & 0 deletions test/forge/helpers/SigUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import {AUTHORIZATION_TYPEHASH} from "src/Blue.sol";

library SigUtils {
struct Authorization {
address authorizer;
address authorized;
bool isAuthorized;
uint256 nonce;
uint256 deadline;
}

/// @dev Computes the hash of the EIP-712 encoded data.
function getTypedDataHash(bytes32 domainSeparator, Authorization memory authorization)
public
pure
returns (bytes32)
{
return keccak256(abi.encodePacked("\x19\x01", domainSeparator, hashStruct(authorization)));
}

function hashStruct(Authorization memory authorization) internal pure returns (bytes32) {
return keccak256(
abi.encode(
AUTHORIZATION_TYPEHASH,
authorization.authorizer,
authorization.authorized,
authorization.isAuthorized,
authorization.nonce,
authorization.deadline
)
);
}
}

0 comments on commit 152087d

Please sign in to comment.