Skip to content

Commit 09d4144

Browse files
kkirkaExef
authored andcommitted
feat: add backend signature verification to the swapper
1 parent cfa9d4e commit 09d4144

File tree

4 files changed

+188
-25
lines changed

4 files changed

+188
-25
lines changed

contracts/SwapHelper/SwapHelper.sol

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,37 @@ pragma solidity 0.8.25;
33

44
import { SafeERC20Upgradeable, IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
55
import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
6+
import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
7+
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
68

79
import { IWBNB } from "../Interfaces.sol";
810

9-
contract SwapHelper {
11+
contract SwapHelper is EIP712 {
1012
using SafeERC20Upgradeable for IERC20Upgradeable;
1113
using AddressUpgradeable for address;
1214

1315
uint256 internal constant REENTRANCY_LOCK_UNLOCKED = 1;
1416
uint256 internal constant REENTRANCY_LOCK_LOCKED = 2;
17+
bytes32 internal constant MULTICALL_TYPEHASH = keccak256("Multicall(bytes[] calls,uint256 deadline)");
1518

1619
/// @notice Wrapped native asset
1720
IWBNB public immutable WRAPPED_NATIVE;
1821

22+
/// @notice Venus backend signer
23+
address public immutable BACKEND_SIGNER;
24+
1925
/// @dev Reentrancy lock to prevent reentrancy attacks
2026
uint256 private reentrancyLock;
2127

2228
/// @notice Error thrown when reentrancy is detected
2329
error Reentrancy();
2430

31+
/// @notice Error thrown when deadline is reached
32+
error DeadlineReached();
33+
34+
/// @notice Error thrown when caller is not the authorized backend signer
35+
error Unauthorized();
36+
2537
/// @notice In the locked state, allow contract to call itself, but block all external calls
2638
modifier externalLock() {
2739
bool isExternal = msg.sender != address(this);
@@ -36,15 +48,37 @@ contract SwapHelper {
3648
if (isExternal) reentrancyLock = REENTRANCY_LOCK_UNLOCKED;
3749
}
3850

39-
constructor(address wrappedNative_) {
51+
/// @notice Constructor
52+
/// @param wrappedNative_ Address of the wrapped native asset
53+
/// @param backendSigner_ Address of the backend signer
54+
constructor(address wrappedNative_, address backendSigner_) EIP712("VenusSwap", "1") {
4055
WRAPPED_NATIVE = IWBNB(wrappedNative_);
56+
BACKEND_SIGNER = backendSigner_;
4157
}
4258

4359
/// @notice Multicall function to execute multiple calls in a single transaction
44-
/// @param data Array of calldata to execute
45-
function multicall(bytes[] calldata data) external payable {
46-
for (uint256 i = 0; i < data.length; i++) {
47-
address(this).functionCall(data[i]);
60+
/// @param calls Array of calldata to execute
61+
/// @param deadline Deadline for the transaction
62+
/// @param signature Backend signature
63+
function multicall(
64+
bytes[] calldata calls,
65+
uint256 deadline,
66+
bytes calldata signature
67+
) external payable externalLock {
68+
if (block.timestamp > deadline) {
69+
revert DeadlineReached();
70+
}
71+
72+
if (signature.length != 0) {
73+
bytes32 digest = _hashMulticall(calls, deadline);
74+
address signer = ECDSA.recover(digest, signature);
75+
if (signer != BACKEND_SIGNER) {
76+
revert Unauthorized();
77+
}
78+
}
79+
80+
for (uint256 i = 0; i < calls.length; i++) {
81+
address(this).functionCall(calls[i]);
4882
}
4983
}
5084

@@ -75,4 +109,19 @@ contract SwapHelper {
75109
token.forceApprove(spender, 0);
76110
token.forceApprove(spender, type(uint256).max);
77111
}
112+
113+
/// @notice Produces an EIP-712 digest of the multicall data
114+
/// @param calls Array of calldata to execute
115+
/// @param deadline Deadline for the transaction
116+
/// @return Digest of the multicall data
117+
function _hashMulticall(bytes[] calldata calls, uint256 deadline) internal view returns (bytes32) {
118+
bytes32[] memory callHashes = new bytes32[](calls.length);
119+
for (uint256 i = 0; i < calls.length; i++) {
120+
callHashes[i] = keccak256(calls[i]);
121+
}
122+
return
123+
_hashTypedDataV4(
124+
keccak256(abi.encode(MULTICALL_TYPEHASH, keccak256(abi.encodePacked(callHashes)), deadline))
125+
);
126+
}
78127
}

contracts/test/Imports.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
12
pragma solidity 0.5.16;
3+
24
import { ComptrollerMock } from "@venusprotocol/venus-protocol/contracts/test/ComptrollerMock.sol";
35
import { VBNB } from "@venusprotocol/venus-protocol/contracts/Tokens/VTokens/VBNB.sol";
46
import { Diamond } from "@venusprotocol/venus-protocol/contracts/Comptroller/Diamond/Diamond.sol";
57
import { InterestRateModelHarness } from "@venusprotocol/venus-protocol/contracts/test/InterestRateModelHarness.sol";
68
import { MockVBNB } from "@venusprotocol/venus-protocol/contracts/test/MockVBNB.sol";
79
import { VBep20Harness } from "@venusprotocol/venus-protocol/contracts/test/VBep20Harness.sol";
810
import { ComptrollerLens } from "@venusprotocol/venus-protocol/contracts/Lens/ComptrollerLens.sol";
11+
import { FaucetToken } from "@venusprotocol/venus-protocol/contracts/test/FaucetToken.sol";

contracts/test/ImportsV8.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
12
pragma solidity 0.8.25;
23

34
import { IAccessControlManagerV8 } from "@venusprotocol/governance-contracts/contracts/Governance/IAccessControlManagerV8.sol";
Lines changed: 129 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,152 @@
11
import { expect } from "chai";
22
import { Signer } from "ethers";
3-
import { parseUnits } from "ethers/lib/utils";
4-
import { ethers } from "hardhat";
3+
import { _TypedDataEncoder, parseUnits } from "ethers/lib/utils";
4+
import { ethers, network } from "hardhat";
55

6-
import { SwapHelper, WBNB } from "../../../typechain";
6+
import { FaucetToken, SwapHelper, WBNB } from "../../../typechain";
77

88
describe("SwapHelper", () => {
9+
const maxUint256 = ethers.constants.MaxUint256;
10+
let owner: Signer;
911
let user1: Signer;
12+
let user2: Signer;
1013
let userAddress: string;
14+
let user2Address: string;
1115
let swapHelper: SwapHelper;
1216
let wBNB: WBNB;
17+
let erc20: FaucetToken;
1318

1419
beforeEach(async () => {
15-
[user1] = await ethers.getSigners();
20+
[owner, user1, user2] = await ethers.getSigners();
1621
userAddress = await user1.getAddress();
22+
user2Address = await user2.getAddress();
1723

1824
const WBNBFactory = await ethers.getContractFactory("WBNB");
1925
wBNB = await WBNBFactory.deploy();
2026

27+
const ERC20Factory = await ethers.getContractFactory("FaucetToken");
28+
erc20 = await ERC20Factory.deploy(parseUnits("10000", 18), "Test Token", 18, "TEST");
29+
2130
const SwapHelperFactory = await ethers.getContractFactory("SwapHelper");
22-
swapHelper = await SwapHelperFactory.deploy(wBNB.address);
31+
swapHelper = await SwapHelperFactory.deploy(wBNB.address, await owner.getAddress());
32+
});
33+
34+
describe("wrap", () => {
35+
it("should only work within multicall", async () => {
36+
const amount = parseUnits("1", 18);
37+
expect(await wBNB.balanceOf(userAddress)).to.equal(0);
38+
const wrapData = await swapHelper.populateTransaction.wrap(amount);
39+
await swapHelper.connect(user1).multicall([wrapData.data!], maxUint256, "0x", { value: amount });
40+
expect(await wBNB.balanceOf(swapHelper.address)).to.equal(amount);
41+
});
42+
});
43+
44+
describe("sweep", () => {
45+
it("should sweep ERC20 tokens to specified address", async () => {
46+
const amount = parseUnits("1000", 18);
47+
await erc20.connect(owner).transfer(swapHelper.address, amount);
48+
expect(await erc20.balanceOf(swapHelper.address)).to.equal(amount);
49+
expect(await erc20.balanceOf(userAddress)).to.equal(0);
50+
51+
await swapHelper.connect(user1).sweep(erc20.address, userAddress);
52+
expect(await erc20.balanceOf(swapHelper.address)).to.equal(0);
53+
expect(await erc20.balanceOf(userAddress)).to.equal(amount);
54+
});
55+
56+
it("should work within multicall", async () => {
57+
const amount = parseUnits("1000", 18);
58+
await erc20.connect(owner).transfer(swapHelper.address, amount);
59+
const sweepData = await swapHelper.populateTransaction.sweep(erc20.address, userAddress);
60+
await swapHelper.connect(user1).multicall([sweepData.data!], maxUint256, "0x");
61+
expect(await erc20.balanceOf(swapHelper.address)).to.equal(0);
62+
expect(await erc20.balanceOf(userAddress)).to.equal(amount);
63+
});
64+
65+
it("should wrap and transfer all in a single call", async () => {
66+
const amount = parseUnits("1", 18);
67+
expect(await wBNB.balanceOf(userAddress)).to.equal(0);
68+
const wrapData = await swapHelper.populateTransaction.wrap(amount);
69+
const sweepData = await swapHelper.populateTransaction.sweep(wBNB.address, userAddress);
70+
await swapHelper.connect(user1).multicall([wrapData.data!, sweepData.data!], maxUint256, "0x", { value: amount });
71+
expect(await wBNB.balanceOf(swapHelper.address)).to.equal(0);
72+
expect(await wBNB.balanceOf(userAddress)).to.equal(amount);
73+
});
2374
});
2475

25-
it("should wrap native BNB into WBNB", async () => {
26-
const amount = parseUnits("1", 18);
27-
expect(await wBNB.balanceOf(userAddress)).to.equal(0);
28-
const wrapData = await swapHelper.populateTransaction.wrap(amount);
29-
expect(await swapHelper.connect(user1).multicall([wrapData.data!], { value: amount }));
30-
expect(await wBNB.balanceOf(swapHelper.address)).to.equal(amount);
76+
describe("approveMax", () => {
77+
it("should approve maximum amount to a spender", async () => {
78+
const spender = user2Address;
79+
expect(await erc20.allowance(swapHelper.address, spender)).to.equal(0);
80+
await swapHelper.connect(user1).approveMax(erc20.address, spender);
81+
expect(await erc20.allowance(swapHelper.address, spender)).to.equal(maxUint256);
82+
});
83+
84+
it("should work within multicall", async () => {
85+
const spender = user2Address;
86+
const approveData = await swapHelper.populateTransaction.approveMax(erc20.address, spender);
87+
await swapHelper.connect(user1).multicall([approveData.data!], maxUint256, "0x");
88+
expect(await erc20.allowance(swapHelper.address, spender)).to.equal(maxUint256);
89+
});
3190
});
3291

33-
it("should wrap and transfer all in a single call", async () => {
34-
const amount = parseUnits("1", 18);
35-
expect(await wBNB.balanceOf(userAddress)).to.equal(0);
36-
const wrapData = await swapHelper.populateTransaction.wrap(amount);
37-
const sweepData = await swapHelper.populateTransaction.sweep(wBNB.address, userAddress);
38-
expect(await swapHelper.connect(user1).multicall([wrapData.data!, sweepData.data!], { value: amount }));
39-
expect(await wBNB.balanceOf(swapHelper.address)).to.equal(0);
40-
expect(await wBNB.balanceOf(userAddress)).to.equal(amount);
92+
describe("multicall", () => {
93+
const types = {
94+
Multicall: [
95+
{ name: "calls", type: "bytes[]" },
96+
{ name: "deadline", type: "uint256" },
97+
],
98+
};
99+
100+
it("should revert if deadline is in the past", async () => {
101+
const amount = parseUnits("1", 18);
102+
const wrapData = await swapHelper.populateTransaction.wrap(amount);
103+
await expect(
104+
swapHelper.connect(user1).multicall([wrapData.data!], 1234, "0x", { value: amount }),
105+
).to.be.revertedWithCustomError(swapHelper, "DeadlineReached");
106+
});
107+
108+
it("should check signature if provided", async () => {
109+
const domain = {
110+
chainId: network.config.chainId,
111+
name: "VenusSwap",
112+
verifyingContract: swapHelper.address,
113+
version: "1",
114+
};
115+
const singleAmount = parseUnits("1", 18);
116+
const totalAmount = singleAmount.mul(3);
117+
const wrapData = await swapHelper.populateTransaction.wrap(singleAmount);
118+
const calls = [wrapData.data!, wrapData.data!, wrapData.data!];
119+
const deadline = maxUint256;
120+
const signature = await owner._signTypedData(domain, types, { calls, deadline });
121+
await swapHelper.connect(user1).multicall(calls, deadline, signature, { value: totalAmount });
122+
expect(await wBNB.balanceOf(swapHelper.address)).to.equal(totalAmount);
123+
});
124+
125+
it("should revert if the signature is invalid", async () => {
126+
const domain = {
127+
chainId: network.config.chainId,
128+
name: "VenusSwap",
129+
verifyingContract: swapHelper.address,
130+
version: "1",
131+
};
132+
const singleAmount = parseUnits("1", 18);
133+
const totalAmount = singleAmount.mul(3);
134+
const wrapData = await swapHelper.populateTransaction.wrap(singleAmount);
135+
const deadline = maxUint256;
136+
const signature = await owner._signTypedData(domain, types, {
137+
// authorizing just one call, not 3
138+
calls: [wrapData.data!],
139+
deadline,
140+
});
141+
await expect(
142+
swapHelper.connect(user1).multicall(
143+
// trying to execute 3 txs, but only 1 is authorized
144+
[wrapData.data!, wrapData.data!, wrapData.data!],
145+
deadline,
146+
signature,
147+
{ value: totalAmount },
148+
),
149+
).to.be.revertedWithCustomError(swapHelper, "Unauthorized");
150+
});
41151
});
42152
});

0 commit comments

Comments
 (0)