Skip to content

Commit cfa9d4e

Browse files
kkirkaExef
authored andcommitted
feat: add generic swapper contract
1 parent 0c5d144 commit cfa9d4e

File tree

2 files changed

+120
-0
lines changed

2 files changed

+120
-0
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// SPDX-License-Identifier: BSD-3-Clause
2+
pragma solidity 0.8.25;
3+
4+
import { SafeERC20Upgradeable, IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
5+
import { AddressUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol";
6+
7+
import { IWBNB } from "../Interfaces.sol";
8+
9+
contract SwapHelper {
10+
using SafeERC20Upgradeable for IERC20Upgradeable;
11+
using AddressUpgradeable for address;
12+
13+
uint256 internal constant REENTRANCY_LOCK_UNLOCKED = 1;
14+
uint256 internal constant REENTRANCY_LOCK_LOCKED = 2;
15+
16+
/// @notice Wrapped native asset
17+
IWBNB public immutable WRAPPED_NATIVE;
18+
19+
/// @dev Reentrancy lock to prevent reentrancy attacks
20+
uint256 private reentrancyLock;
21+
22+
/// @notice Error thrown when reentrancy is detected
23+
error Reentrancy();
24+
25+
/// @notice In the locked state, allow contract to call itself, but block all external calls
26+
modifier externalLock() {
27+
bool isExternal = msg.sender != address(this);
28+
29+
if (isExternal) {
30+
if (reentrancyLock == REENTRANCY_LOCK_LOCKED) revert Reentrancy();
31+
reentrancyLock = REENTRANCY_LOCK_LOCKED;
32+
}
33+
34+
_;
35+
36+
if (isExternal) reentrancyLock = REENTRANCY_LOCK_UNLOCKED;
37+
}
38+
39+
constructor(address wrappedNative_) {
40+
WRAPPED_NATIVE = IWBNB(wrappedNative_);
41+
}
42+
43+
/// @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]);
48+
}
49+
}
50+
51+
/// @notice Generic call function to execute a call to an arbitrary address
52+
/// @param target Address to call
53+
/// @param data Calldata to execute
54+
function genericCall(address target, bytes calldata data) external externalLock {
55+
target.functionCall(data);
56+
}
57+
58+
/// @notice Wraps native asset into an ERC-20 token
59+
/// @param amount Amount of native asset to wrap
60+
function wrap(uint256 amount) external externalLock {
61+
WRAPPED_NATIVE.deposit{ value: amount }();
62+
}
63+
64+
/// @notice Sweeps an ERC-20 token to a specified address
65+
/// @param token ERC-20 token to sweep
66+
/// @param to Address to send the token to
67+
function sweep(IERC20Upgradeable token, address to) external externalLock {
68+
token.safeTransfer(to, token.balanceOf(address(this)));
69+
}
70+
71+
/// @notice Approves the maximum amount of an ERC-20 token to a specified address
72+
/// @param token ERC-20 token to approve
73+
/// @param spender Address to approve the token to
74+
function approveMax(IERC20Upgradeable token, address spender) external externalLock {
75+
token.forceApprove(spender, 0);
76+
token.forceApprove(spender, type(uint256).max);
77+
}
78+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { expect } from "chai";
2+
import { Signer } from "ethers";
3+
import { parseUnits } from "ethers/lib/utils";
4+
import { ethers } from "hardhat";
5+
6+
import { SwapHelper, WBNB } from "../../../typechain";
7+
8+
describe("SwapHelper", () => {
9+
let user1: Signer;
10+
let userAddress: string;
11+
let swapHelper: SwapHelper;
12+
let wBNB: WBNB;
13+
14+
beforeEach(async () => {
15+
[user1] = await ethers.getSigners();
16+
userAddress = await user1.getAddress();
17+
18+
const WBNBFactory = await ethers.getContractFactory("WBNB");
19+
wBNB = await WBNBFactory.deploy();
20+
21+
const SwapHelperFactory = await ethers.getContractFactory("SwapHelper");
22+
swapHelper = await SwapHelperFactory.deploy(wBNB.address);
23+
});
24+
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);
31+
});
32+
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);
41+
});
42+
});

0 commit comments

Comments
 (0)