Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Spender permit feature #110

Open
wants to merge 60 commits into
base: master
Choose a base branch
from
Open
Changes from 20 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
d1d0156
feat: spender contract and foundry test
oneleo Oct 19, 2022
93833a5
Merge branch 'master' into LABS-960/rfq-with-spender-permit
oneleo Oct 19, 2022
ecbe62e
bug: change to new signature format
oneleo Oct 24, 2022
b05f0ec
feat: replace rfq spendfromuser func with spendfromusertowithpermit
oneleo Oct 24, 2022
a05cfcd
refac: leverage struct spendwithpermit in params
oneleo Oct 26, 2022
eae8717
chg: modify variable names for readability
oneleo Oct 26, 2022
d89c922
refac: move require condition from rfq to spender contract
oneleo Oct 26, 2022
89c376a
refac: modify variables and function in rfq foundry test
oneleo Oct 26, 2022
8af0b60
chg: modify variable names for readability
oneleo Oct 26, 2022
b5b3793
spec: Add requester verification foundry test
oneleo Oct 26, 2022
b2f296b
refac: leverage struct spendwithpermit in params
oneleo Oct 26, 2022
9dca7b1
bug: assign replay protection check before sig valid check
oneleo Oct 26, 2022
c376229
bug: use safeTransfer() in SafeERC20 instead
oneleo Oct 26, 2022
2d21eaa
feat: remove taker/maker spendWithPermit but create they from _order
oneleo Oct 26, 2022
d493f7c
refac: convert the BPS_MAX variable appropriately
oneleo Oct 26, 2022
31ad782
refac: move taker/maker spendWithPermit from fill() to _settle()
oneleo Oct 27, 2022
ac38d3e
feat: modify error message returned when replay protection checks
oneleo Oct 27, 2022
123e7df
refac: use only libconstant.bps_max variable
oneleo Oct 27, 2022
c310fa1
chg: run prettier to format spender.sol to resolve code style issues
oneleo Oct 27, 2022
556ddc7
Merge pull request #99 from consenlabs/LABS-940/spender-adds-backward…
oneleo Oct 27, 2022
65d0d60
Merge branch 'master' into modify-salt-usage-of-spendwithpermit
oneleo Oct 27, 2022
9f242b9
feat: spendWithPermit should contain transaction or order hash
oneleo Oct 28, 2022
45e146f
Merge branch 'master' into spender_permit_feature
oneleo Oct 28, 2022
049cda6
Merge branch 'spender_permit_feature' into modify-salt-usage-of-spend…
oneleo Oct 28, 2022
8acfb68
Merge pull request #105 from consenlabs/modify-salt-usage-of-spendwit…
oneleo Oct 31, 2022
c8b8f2d
Merge branch 'master' into spender_permit_feature
oneleo Nov 4, 2022
2a5da3d
feat: spendWithPermit should contain transaction or order hash
oneleo Nov 4, 2022
36d2730
Merge pull request #111 from consenlabs/modify-txhash-name-of-spendwi…
charlesjhongc Nov 7, 2022
db4726e
refac: rfq, spender contracts changed to non named parameter type
oneleo Nov 7, 2022
db1564a
Merge branch 'master' into spender_permit_feature
oneleo Nov 7, 2022
03d5b72
Merge pull request #112 from consenlabs/rfq_spender_to_no_named_param…
oneleo Nov 8, 2022
77fb55a
refac: move the signSpendWithPermit function in foundry test
oneleo Nov 9, 2022
5627a14
perf: use eip712 domain separator instead of spender contract
oneleo Nov 9, 2022
b9f4bac
bug: revert when sigType is invalid
oneleo Nov 9, 2022
9a1d452
modify spendFromUser --> spendFromUserToWithPermit
Nov 10, 2022
a130849
add helper function regarding spenderPermit on l2Deposit action
Nov 10, 2022
f17e7ab
modify test case using spenderPermit
Nov 10, 2022
0714df3
replace comment for _createSpenderPermitFromL2Deposit
Nov 10, 2022
c3377c7
remove accidentally added files
Nov 10, 2022
f02e23a
remove redundant comments
Nov 10, 2022
4dd27c9
fix nit
Nov 11, 2022
7b4392e
modify solidity objects using named parameters
Nov 11, 2022
be48682
modify DEFAULT_DEPOSIT to _deposit in _createSpenderPermitFromL2Deposit
Nov 11, 2022
b66e418
Merge pull request #114 from consenlabs/move_signspendwithpermit_func
oneleo Nov 11, 2022
6bdf206
feat: replace amm spendfromuser func with spendfromusertowithpermit
oneleo Nov 11, 2022
a52be1c
chg: modify variable name to match internal variable format
oneleo Nov 11, 2022
7150101
Merge branch 'spender_permit_feature' into modify-l2deposit-using-spe…
Nov 14, 2022
a0e582e
remove signSpendWithPermit() from Setup.t.sol
Nov 14, 2022
4130802
use signSpendWithPermit() from contracts-test/utils/Permit.sol
Nov 14, 2022
58d1b78
refec: modify comments and function names
oneleo Nov 14, 2022
4aa11d0
Merge pull request #115 from consenlabs/modify-l2deposit-using-spende…
108356037 Nov 14, 2022
eb9ca88
refec: modify comments and remove unused imported package
oneleo Nov 14, 2022
66b606f
doc: adject _transferTakerAssetToAMM func comment to be clearer
oneleo Nov 15, 2022
2619aca
doc: use named parameters to make code clearer
oneleo Nov 15, 2022
6c542db
doc: apply named parameters to transferTakerAssetToAMM in ammwrapper
oneleo Nov 15, 2022
424feec
Merge pull request #116 from consenlabs/amm-with-spender-permit
oneleo Nov 15, 2022
a67eba2
refac: handle the conflict between RFQ.t.sol and the master branch
oneleo Nov 16, 2022
e8b9284
Merge branch 'master' into spender_permit_feature
oneleo Nov 16, 2022
8dcef47
feat: remove spendFromUser() func in Spender.sol
oneleo Nov 16, 2022
d1e7f20
Merge pull request #119 from consenlabs/remove_spendfromuser_function
charlesjhongc Feb 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 48 additions & 10 deletions contracts/RFQ.sol
Original file line number Diff line number Diff line change
@@ -2,6 +2,8 @@
pragma solidity 0.7.6;
pragma abicoder v2;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Address.sol";
@@ -10,12 +12,14 @@ import "./interfaces/IRFQ.sol";
import "./utils/StrategyBase.sol";
import "./utils/RFQLibEIP712.sol";
import "./utils/BaseLibEIP712.sol";
import "./utils/SpenderLibEIP712.sol";
import "./utils/SignatureValidator.sol";
import "./utils/LibConstant.sol";

contract RFQ is IRFQ, StrategyBase, ReentrancyGuard, SignatureValidator, BaseLibEIP712 {
using SafeMath for uint256;
using Address for address;
using SafeERC20 for IERC20;

// Constants do not have storage slot.
string public constant SOURCE = "RFQ v1";
@@ -81,7 +85,9 @@ contract RFQ is IRFQ, StrategyBase, ReentrancyGuard, SignatureValidator, BaseLib
function fill(
RFQLibEIP712.Order calldata _order,
bytes calldata _mmSignature,
bytes calldata _userSignature
bytes calldata _userSignature,
bytes calldata _makerAssetPermitSig,
bytes calldata _takerAssetPermitSig
) external payable override nonReentrant onlyUserProxy returns (uint256) {
// check the order deadline and fee factor
require(_order.deadline >= block.timestamp, "RFQ: expired order");
@@ -98,7 +104,7 @@ contract RFQ is IRFQ, StrategyBase, ReentrancyGuard, SignatureValidator, BaseLib
// Set transaction as seen, PermanentStorage would throw error if transaction already seen.
permStorage.setRFQTransactionSeen(vars.transactionHash);

return _settle(_order, vars);
return _settle({ _order: _order, _vars: vars, _makerAssetPermitSig: _makerAssetPermitSig, _takerAssetPermitSig: _takerAssetPermitSig });
}

function _emitFillOrder(
@@ -123,15 +129,45 @@ contract RFQ is IRFQ, StrategyBase, ReentrancyGuard, SignatureValidator, BaseLib
}

// settle
function _settle(RFQLibEIP712.Order memory _order, GroupedVars memory _vars) internal returns (uint256) {
function _settle(
RFQLibEIP712.Order memory _order,
GroupedVars memory _vars,
bytes memory _makerAssetPermitSig,
bytes memory _takerAssetPermitSig
) internal returns (uint256) {
// Declare the 'maker sends makerAsset to this contract' SpendWithPermit struct from _order parameter
SpenderLibEIP712.SpendWithPermit memory makerAssetPermit = SpenderLibEIP712.SpendWithPermit({
tokenAddr: _order.makerAssetAddr,
requester: address(this),
user: _order.makerAddr,
recipient: address(this),
amount: _order.makerAssetAmount,
salt: _order.salt,
expiry: uint64(_order.deadline)
});

// Declare the 'taker sends takerAsset to this contract' SpendWithPermit struct from _order parameter
SpenderLibEIP712.SpendWithPermit memory takerAssetPermit = SpenderLibEIP712.SpendWithPermit({
tokenAddr: _order.takerAssetAddr,
requester: address(this),
user: _order.takerAddr,
recipient: address(this),
amount: _order.takerAssetAmount,
salt: _order.salt,
expiry: uint64(_order.deadline)
});

// Transfer taker asset to maker
if (address(weth) == _order.takerAssetAddr) {
// Deposit to WETH if taker asset is ETH
require(msg.value == _order.takerAssetAmount, "RFQ: insufficient ETH");
weth.deposit{ value: msg.value }();
weth.transfer(_order.makerAddr, _order.takerAssetAmount);
} else {
spender.spendFromUserTo(_order.takerAddr, _order.takerAssetAddr, _order.makerAddr, _order.takerAssetAmount);
// Transfer taker asset to this contract first,
spender.spendFromUserToWithPermit({ _params: takerAssetPermit, _spendWithPermitSig: _takerAssetPermitSig });
// then transfer from this to maker.
IERC20(_order.takerAssetAddr).safeTransfer(_order.makerAddr, _order.takerAssetAmount);
}

// Transfer maker asset to taker, sub fee
@@ -141,18 +177,20 @@ contract RFQ is IRFQ, StrategyBase, ReentrancyGuard, SignatureValidator, BaseLib
settleAmount = settleAmount.sub(fee);
}

// Transfer token/Eth to receiver
// Transfer maker asset to this contract first
spender.spendFromUserToWithPermit({ _params: makerAssetPermit, _spendWithPermitSig: _makerAssetPermitSig });

// Transfer maker asset less fee from this contract to receiver
if (_order.makerAssetAddr == address(weth)) {
// Transfer from maker
spender.spendFromUser(_order.makerAddr, _order.makerAssetAddr, settleAmount);
weth.withdraw(settleAmount);
payable(_order.receiverAddr).transfer(settleAmount);
} else {
spender.spendFromUserTo(_order.makerAddr, _order.makerAssetAddr, _order.receiverAddr, settleAmount);
IERC20(_order.makerAssetAddr).safeTransfer(_order.receiverAddr, settleAmount);
}
// Collect fee

// Transfer fee from this contract to feeCollector
if (fee > 0) {
spender.spendFromUserTo(_order.makerAddr, _order.makerAssetAddr, feeCollector, fee);
IERC20(_order.makerAssetAddr).safeTransfer(feeCollector, fee);
}

_emitFillOrder(_order, _vars, settleAmount);
31 changes: 30 additions & 1 deletion contracts/Spender.sol
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;
pragma abicoder v2;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";

import "./utils/LibConstant.sol";
import "./utils/Ownable.sol";
import "./utils/SpenderLibEIP712.sol";
import "./utils/BaseLibEIP712.sol";
import "./utils/SignatureValidator.sol";
import "./interfaces/ISpender.sol";
import "./interfaces/IAllowanceTarget.sol";

/**
* @dev Spender contract
*/
contract Spender is ISpender, Ownable {
contract Spender is ISpender, Ownable, BaseLibEIP712, SignatureValidator {
using SafeMath for uint256;

// Constants do not have storage slot.
@@ -32,6 +36,7 @@ contract Spender is ISpender, Ownable {
mapping(address => bool) public consumeGasERC20Tokens;
mapping(uint256 => address) public pendingAuthorized;
mapping(address => bool) private authorized;
mapping(bytes32 => bool) private spendingFulfilled; // Store the fulfilled spending hash
mapping(address => bool) private tokenBlacklist;

/************************************************************
@@ -190,6 +195,30 @@ contract Spender is ISpender, Ownable {
_transferTokenFromUserTo(_user, _tokenAddr, _recipient, _amount);
}

/// @dev Spend tokens on user's behalf with user's permit signature. Only an authority can call this.
/// @param _params The params of the SpendWithPermit.
/// @param _spendWithPermitSig Spend with permit signature.
function spendFromUserToWithPermit(SpenderLibEIP712.SpendWithPermit calldata _params, bytes calldata _spendWithPermitSig) external override onlyAuthorized {
// Check expiry timestamp
require(_params.expiry >= block.timestamp, "Spender: Permit is expired");
// Check requester validity
require(_params.requester == msg.sender, "Spender: invalid requester address");

// Validate spend with permit signature
bytes32 spendWithPermitHash = getEIP712Hash({ structHash: SpenderLibEIP712._getSpendWithPermitHash({ _spendWithPermit: _params }) });

// Validate spending is not replayed
require(!spendingFulfilled[spendWithPermitHash], "Spender: Permit is already fulfilled");
spendingFulfilled[spendWithPermitHash] = true;

require(
isValidSignature({ _signerAddress: _params.user, _hash: spendWithPermitHash, _data: bytes(""), _sig: _spendWithPermitSig }),
"Spender: Invalid permit signature"
);

_transferTokenFromUserTo(_params.user, _params.tokenAddr, _params.recipient, _params.amount);
}

function _transferTokenFromUserTo(
address _user,
address _tokenAddr,
18 changes: 9 additions & 9 deletions contracts/SpenderSimulation.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;
pragma abicoder v2;

import "./interfaces/IHasBlackListERC20Token.sol";
import "./interfaces/ISpender.sol";
import "./utils/SpenderLibEIP712.sol";

contract SpenderSimulation {
ISpender public immutable spender;
@@ -33,15 +35,13 @@ contract SpenderSimulation {
*************************************************************/
/// @dev Spend tokens on user's behalf but reverts if succeed.
/// This is only intended to be run off-chain to check if the transfer will succeed.
/// @param _user The user to spend token from.
/// @param _tokenAddr The address of the token.
/// @param _amount Amount to spend.
function simulate(
address _user,
address _tokenAddr,
uint256 _amount
) external checkBlackList(_tokenAddr, _user) {
spender.spendFromUser(_user, _tokenAddr, _amount);
/// @param _params The params of the SpendWithPermit.
/// @param _spendWithPermitSig Spend with permit signature.
function simulate(SpenderLibEIP712.SpendWithPermit calldata _params, bytes calldata _spendWithPermitSig)
external
checkBlackList(_params.tokenAddr, _params.user)
{
spender.spendFromUserToWithPermit({ _params: _params, _spendWithPermitSig: _spendWithPermitSig });

// All checks passed: revert with success reason string
revert("SpenderSimulation: transfer simulation success");
9 changes: 6 additions & 3 deletions contracts/interfaces/IRFQ.sol
Original file line number Diff line number Diff line change
@@ -4,11 +4,14 @@ pragma abicoder v2;

import "./IStrategyBase.sol";
import "../utils/RFQLibEIP712.sol";
import "../utils/SpenderLibEIP712.sol";

interface IRFQ is IStrategyBase {
function fill(
RFQLibEIP712.Order memory _order,
bytes memory _mmSignature,
bytes memory _userSignature
RFQLibEIP712.Order calldata _order,
bytes calldata _mmSignature,
bytes calldata _userSignature,
bytes calldata _makerAssetPermitSig,
bytes calldata _takerAssetPermitSig
) external payable returns (uint256);
}
5 changes: 5 additions & 0 deletions contracts/interfaces/ISpender.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0;
pragma abicoder v2;

import "../utils/SpenderLibEIP712.sol";

interface ISpender {
// System events
@@ -24,4 +27,6 @@ interface ISpender {
address _receiverAddr,
uint256 _amount
) external;

function spendFromUserToWithPermit(SpenderLibEIP712.SpendWithPermit calldata _params, bytes calldata _spendWithPermitSig) external;
}
212 changes: 186 additions & 26 deletions contracts/test/RFQ.t.sol

Large diffs are not rendered by default.

184 changes: 174 additions & 10 deletions contracts/test/Spender.t.sol
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.7.6;
pragma abicoder v2;

import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "contracts/Spender.sol";
import "contracts/SpenderSimulation.sol";
import "contracts/AllowanceTarget.sol";
import "contracts/utils/SpenderLibEIP712.sol";
import "contracts-test/mocks/MockERC20.sol";
import "contracts-test/mocks/MockDeflationaryERC20.sol";
import "contracts-test/mocks/MockNoReturnERC20.sol";
import "contracts-test/mocks/MockNoRevertERC20.sol";
import "contracts-test/utils/BalanceUtil.sol";
import "contracts-test/utils/BalanceSnapshot.sol";

contract SpenderTest is BalanceUtil {
using BalanceSnapshot for BalanceSnapshot.Snapshot;
using SafeERC20 for IERC20;

event TearDownAllowanceTarget(uint256 tearDownTimeStamp);
struct SpendWithPermit {
address tokenAddr;
address user;
address recipient;
uint256 amount;
uint256 salt;
uint64 expiry;
}

uint256 userPrivateKey = uint256(1);
uint256 otherPrivateKey = uint256(2);

address user = vm.addr(userPrivateKey);
// Originally requester should be the address of the strategy contract
// that calls spender; But in this test case, the requester is address(this)
address requester = address(this);
address recipient = address(0x133702);
address unauthorized = address(0x133704);
address[] wallet = [address(this), user, recipient];

Spender spender;
SpenderSimulation spenderSimulation;
AllowanceTarget allowanceTarget;
MockERC20 lon = new MockERC20("TOKENLON", "LON", 18);
MockDeflationaryERC20 deflationaryERC20 = new MockDeflationaryERC20();
@@ -42,7 +43,7 @@ contract SpenderTest is BalanceUtil {
IERC20[] tokens = [IERC20(address(deflationaryERC20)), IERC20(address(noReturnERC20)), IERC20(address(noRevertERC20))];

uint64 EXPIRY = uint64(block.timestamp + 1);
SpendWithPermit DEFAULT_SPEND_WITH_PERMIT;
SpenderLibEIP712.SpendWithPermit DEFAULT_SPEND_WITH_PERMIT;

// effectively a "beforeEach" block
function setUp() public {
@@ -55,6 +56,11 @@ contract SpenderTest is BalanceUtil {
// Setup
spender.setAllowanceTarget(address(allowanceTarget));
spender.authorize(wallet);
// Deploy spenderSimulation contract adn set its address to authorization list of spender
spenderSimulation = new SpenderSimulation(spender, new address[](0));
address[] memory spenderSimulationAddr = new address[](1);
spenderSimulationAddr[0] = address(spenderSimulation);
spender.authorize(spenderSimulationAddr);

// Deal 100 ETH to each account
for (uint256 i = 0; i < wallet.length; i++) {
@@ -74,8 +80,9 @@ contract SpenderTest is BalanceUtil {

// Default SpendWithPermit
// prettier-ignore
DEFAULT_SPEND_WITH_PERMIT = SpendWithPermit(
DEFAULT_SPEND_WITH_PERMIT = SpenderLibEIP712.SpendWithPermit(
address(lon), // tokenAddr
requester, // requester
user, // user
recipient, // receipient
100 * 1e18, // amount
@@ -84,11 +91,13 @@ contract SpenderTest is BalanceUtil {
);

// Label addresses for easier debugging
vm.label(requester, "Requester");
vm.label(user, "User");
vm.label(recipient, "Recipient");
vm.label(unauthorized, "Unauthorized");
vm.label(address(this), "TestingContract");
vm.label(address(spender), "SpenderContract");
vm.label(address(spenderSimulation), "SpenderSimulationContract");
vm.label(address(allowanceTarget), "AllowanceTargetContract");
vm.label(address(lon), "LON");
}
@@ -306,4 +315,159 @@ contract SpenderTest is BalanceUtil {

assertEq(noReturnERC20.balanceOf(recipient), 100);
}

/*******************************************
* Test: spendFromUserToWithPermit *
*******************************************/

function testCannotSpendFromUserToWithPermitWithExpiredPermit() public {
SpenderLibEIP712.SpendWithPermit memory spendWithPermit = DEFAULT_SPEND_WITH_PERMIT;
spendWithPermit.expiry = uint64(block.timestamp - 1); // Timestamp expired
bytes memory sig = _signSpendWithPermit(userPrivateKey, spendWithPermit);

vm.expectRevert("Spender: Permit is expired");
spender.spendFromUserToWithPermit(spendWithPermit, sig);
}

function testCannotSpendFromUserToWithPermitWithWrongRequester() public {
SpenderLibEIP712.SpendWithPermit memory spendWithPermit = DEFAULT_SPEND_WITH_PERMIT;
spendWithPermit.requester = unauthorized; // Wrong requester address
bytes memory sig = _signSpendWithPermit(userPrivateKey, spendWithPermit);

vm.expectRevert("Spender: invalid requester address");
spender.spendFromUserToWithPermit(spendWithPermit, sig);
}

function testCannotSpendFromUserToWithPermitByWrongSigner() public {
SpenderLibEIP712.SpendWithPermit memory spendWithPermit = DEFAULT_SPEND_WITH_PERMIT;
bytes memory sig = _signSpendWithPermit(otherPrivateKey, spendWithPermit); // Wrong signer

vm.expectRevert("Spender: Invalid permit signature");
spender.spendFromUserToWithPermit(spendWithPermit, sig);
}

function testCannotSpendFromUserToWithPermitWithWrongRecipient() public {
SpenderLibEIP712.SpendWithPermit memory spendWithPermit = DEFAULT_SPEND_WITH_PERMIT;
bytes memory sig = _signSpendWithPermit(userPrivateKey, spendWithPermit);

vm.expectRevert("Spender: Invalid permit signature");
spendWithPermit.recipient = unauthorized; // recipient is different from signed
spender.spendFromUserToWithPermit(spendWithPermit, sig);
}

function testCannotSpendFromUserToWithPermitWithWrongToken() public {
SpenderLibEIP712.SpendWithPermit memory spendWithPermit = DEFAULT_SPEND_WITH_PERMIT;
bytes memory sig = _signSpendWithPermit(userPrivateKey, spendWithPermit);

vm.expectRevert("Spender: Invalid permit signature");
spendWithPermit.tokenAddr = unauthorized; // tokenAddr is different from signed
spender.spendFromUserToWithPermit(spendWithPermit, sig);
}

function testCannotSpendFromUserToWithPermitWithWrongAmount() public {
SpenderLibEIP712.SpendWithPermit memory spendWithPermit = DEFAULT_SPEND_WITH_PERMIT;
bytes memory sig = _signSpendWithPermit(userPrivateKey, spendWithPermit);

vm.expectRevert("Spender: Invalid permit signature");
spendWithPermit.amount = spendWithPermit.amount + 1; // amount is different from signed
spender.spendFromUserToWithPermit(spendWithPermit, sig);
}

function testCannotSpendFromUserToWithPermitByNotAuthorized() public {
SpenderLibEIP712.SpendWithPermit memory spendWithPermit = DEFAULT_SPEND_WITH_PERMIT;
bytes memory sig = _signSpendWithPermit(userPrivateKey, spendWithPermit);

vm.expectRevert("Spender: not authorized");
vm.prank(unauthorized); // Only authorized strategy contracts and owner
spender.spendFromUserToWithPermit(spendWithPermit, sig);
}

function testCannotSpendFromUserToWithPermitWithBlacklistedToken() public {
address[] memory blacklistAddress = new address[](1);
blacklistAddress[0] = address(lon);
bool[] memory blacklistBool = new bool[](1);
blacklistBool[0] = true;
spender.blacklist(blacklistAddress, blacklistBool); // Set lon to black list by owner (this contract)
SpenderLibEIP712.SpendWithPermit memory spendWithPermit = DEFAULT_SPEND_WITH_PERMIT;
bytes memory sig = _signSpendWithPermit(userPrivateKey, spendWithPermit);

vm.expectRevert("Spender: token is blacklisted");
spender.spendFromUserToWithPermit(spendWithPermit, sig);
}

function testCannotSpendFromUserToWithPermitWithSamePermit() public {
SpenderLibEIP712.SpendWithPermit memory spendWithPermit = DEFAULT_SPEND_WITH_PERMIT;
bytes memory sig = _signSpendWithPermit(userPrivateKey, spendWithPermit);
spender.spendFromUserToWithPermit(spendWithPermit, sig);

vm.expectRevert("Spender: Permit is already fulfilled");
spender.spendFromUserToWithPermit(spendWithPermit, sig); // Detected the same permit hash in the past
}

function testSpendFromUserToWithPermit() public {
BalanceSnapshot.Snapshot memory recipientLon = BalanceSnapshot.take(recipient, address(lon));
SpenderLibEIP712.SpendWithPermit memory spendWithPermit = DEFAULT_SPEND_WITH_PERMIT;
bytes memory sig = _signSpendWithPermit(userPrivateKey, spendWithPermit);
spender.spendFromUserToWithPermit(spendWithPermit, sig);

recipientLon.assertChange(int256(spendWithPermit.amount)); // Confirm amount of tokens received
}

/*******************************************
* Test: simulate *
*******************************************/

function testCannotSimulateByNotAuthorized() public {
SpenderLibEIP712.SpendWithPermit memory spendWithPermit = DEFAULT_SPEND_WITH_PERMIT;
// Set the requester address to spenderSimulation contract
spendWithPermit.requester = address(spenderSimulation);
bytes memory sig = _signSpendWithPermit(userPrivateKey, spendWithPermit);
// Set address of spenderSimulation to be removed from authorization list of spender,
// and spenderSimulation can not execute spender.spendFromUserToWithPermit() function.
address[] memory spenderSimulationAddr = new address[](1);
spenderSimulationAddr[0] = address(spenderSimulation);
spender.deauthorize(spenderSimulationAddr);

vm.expectRevert("Spender: not authorized");
spenderSimulation.simulate(spendWithPermit, sig);
}

function testSimulate() public {
SpenderLibEIP712.SpendWithPermit memory spendWithPermit = DEFAULT_SPEND_WITH_PERMIT;
// Set the requester address to spenderSimulation contract
spendWithPermit.requester = address(spenderSimulation);
bytes memory sig = _signSpendWithPermit(userPrivateKey, spendWithPermit);

vm.expectRevert("SpenderSimulation: transfer simulation success");
spenderSimulation.simulate(spendWithPermit, sig);
}

/*********************************
* Helpers *
*********************************/

function _getEIP712Hash(bytes32 structHash) internal view returns (bytes32) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reused the one in contracts-test/utils/Sig.sol.

string memory EIP191_HEADER = "\x19\x01";
bytes32 EIP712_DOMAIN_SEPARATOR = spender.EIP712_DOMAIN_SEPARATOR();
return keccak256(abi.encodePacked(EIP191_HEADER, EIP712_DOMAIN_SEPARATOR, structHash));
}

function _signSpendWithPermit(uint256 privateKey, SpenderLibEIP712.SpendWithPermit memory spendWithPermit) internal returns (bytes memory sig) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove.

uint256 SPEND_WITH_PERMIT_TYPEHASH = 0xab1af22032364b17f69bad7eabde29f0cd3f761861c0343407be7fcac2e3ff1f;
bytes32 structHash = keccak256(
abi.encode(
SPEND_WITH_PERMIT_TYPEHASH,
spendWithPermit.tokenAddr,
spendWithPermit.requester,
spendWithPermit.user,
spendWithPermit.recipient,
spendWithPermit.amount,
spendWithPermit.salt,
spendWithPermit.expiry
)
);
bytes32 spendWithPermitHash = _getEIP712Hash(structHash);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, spendWithPermitHash);
sig = abi.encodePacked(r, s, v, uint8(2)); // SignatureType = 2 = EIP712
}
}
46 changes: 46 additions & 0 deletions contracts/utils/SpenderLibEIP712.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.6;

library SpenderLibEIP712 {
struct SpendWithPermit {
address tokenAddr;
address requester;
address user;
address recipient;
uint256 amount;
uint256 salt;
uint64 expiry;
}
/*
keccak256(
abi.encodePacked(
"SpendWithPermit(",
"address tokenAddr,",
"address requester,",
"address user,",
"address recipient,",
"uint256 amount,",
"uint256 salt,",
"uint64 expiry",
")"
)
);
*/
uint256 public constant SPEND_WITH_PERMIT_TYPEHASH = 0xab1af22032364b17f69bad7eabde29f0cd3f761861c0343407be7fcac2e3ff1f;

function _getSpendWithPermitHash(SpendWithPermit memory _spendWithPermit) internal pure returns (bytes32) {
return
keccak256(
abi.encode(
SPEND_WITH_PERMIT_TYPEHASH,
_spendWithPermit.tokenAddr,
_spendWithPermit.requester,
_spendWithPermit.user,
_spendWithPermit.recipient,
_spendWithPermit.amount,
_spendWithPermit.salt,
_spendWithPermit.expiry
)
);
}
}