Skip to content

Commit

Permalink
Euphrates router (#42)
Browse files Browse the repository at this point in the history
* forge install: Euphrates

* add EuphratesRouter

* add tests
  • Loading branch information
wangjj9219 authored Jan 11, 2024
1 parent bb1b628 commit 6ce848d
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@
path = lib/predeploy-contracts
url = https://github.com/AcalaNetwork/predeploy-contracts
branch = v4.3.5
[submodule "lib/Euphrates"]
path = lib/Euphrates
url = https://github.com/AcalaNetwork/Euphrates
1 change: 1 addition & 0 deletions lib/Euphrates
Submodule Euphrates added at 92aea7
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ solmate/=lib/solmate/src/
@openzeppelin/=lib/openzeppelin-contracts/
wormhole/=lib/wormhole/ethereum/contracts/
@acala-network/=lib/predeploy-contracts/
euphrates/=lib/Euphrates/src/
20 changes: 20 additions & 0 deletions scripts/deploy-euphrates-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ethers, run } from 'hardhat';

async function main() {
const Factory = await ethers.getContractFactory('EuphratesFactory');
const factory = await Factory.deploy();
await factory.deployed();

console.log(`euphrates factory address: ${factory.address}`);
console.log('remember to publish it!');

await run('verify:verify', {
address: factory.address,
constructorArguments: [],
});
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
62 changes: 62 additions & 0 deletions src/EuphratesFactory.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

import { ERC20 } from "solmate/tokens/ERC20.sol";
import { FeeRegistry } from "./FeeRegistry.sol";
import { EuphratesRouter, EuphratesInstructions } from "./EuphratesRouter.sol";

contract EuphratesFactory {
function deployEuphratesRouter(FeeRegistry fees, EuphratesInstructions memory inst, address euphratesAddress)
public
returns (EuphratesRouter)
{
// no need to use salt as we want to keep the router address the same for the same fees &instructions
bytes32 salt;

EuphratesRouter router;
try new EuphratesRouter{salt: salt}(fees, inst, euphratesAddress) returns (EuphratesRouter router_) {
router = router_;
} catch {
router = EuphratesRouter(
address(
uint160(
uint256(
keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(
abi.encodePacked(type(EuphratesRouter).creationCode, abi.encode(fees, inst))
)
)
)
)
)
)
);
}

return router;
}

function deployEuphratesRouterAndRoute(
FeeRegistry fees,
EuphratesInstructions memory inst,
address euphratesAddress,
ERC20 token
) public {
EuphratesRouter router = deployEuphratesRouter(fees, inst, euphratesAddress);
router.route(token, msg.sender);
}

function deployEuphratesRouterAndRouteNoFee(
FeeRegistry fees,
EuphratesInstructions memory inst,
address euphratesAddress,
ERC20 token
) public {
EuphratesRouter router = deployEuphratesRouter(fees, inst, euphratesAddress);
router.routeNoFee(token);
}
}
43 changes: 43 additions & 0 deletions src/EuphratesRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

import { ERC20 } from "solmate/tokens/ERC20.sol";
import { SafeTransferLib } from "solmate/utils/SafeTransferLib.sol";
import { IStakingTo } from "euphrates/IStaking.sol";
import { BaseRouter } from "./BaseRouter.sol";
import { FeeRegistry } from "./FeeRegistry.sol";

struct EuphratesInstructions {
uint256 poolId;
address recipient;
}

contract EuphratesRouter is BaseRouter {
using SafeTransferLib for ERC20;

address private _euphratesAddress;
EuphratesInstructions private _instructions;

constructor(FeeRegistry fees, EuphratesInstructions memory instructions, address euphratesAddress)
BaseRouter(fees)
{
_instructions = instructions;
_euphratesAddress = euphratesAddress;
}

function routeImpl(ERC20 token) internal override {
if (address(token) == address(IStakingTo(_euphratesAddress).shareTypes(_instructions.poolId))) {
bool approved = token.approve(_euphratesAddress, token.balanceOf(address(this)));
require(approved, "EuphratesRouter: approve failed");

// This may fail due to the configurations of Euphrates.
// That means user is doing something wrong and will revert.
IStakingTo(_euphratesAddress).stakeTo(
_instructions.poolId, token.balanceOf(address(this)), _instructions.recipient
);
} else {
// received token is not share token, transfer it to recipient to avoid it stuck in this contract
token.safeTransfer(_instructions.recipient, token.balanceOf(address(this)));
}
}
}
94 changes: 94 additions & 0 deletions test/EuphratesRouter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";

import "../src/EuphratesRouter.sol";
import "../src/FeeRegistry.sol";
import "./MockToken.sol";
import "./MockEuphrates.sol";

contract EuphratesRouterTest is Test {
FeeRegistry public fees;
MockToken public token1;
MockToken public token2;
MockToken public token3;
MockEuphrates public euphrates;
address public alice = address(0x01010101010101010101);
address public bob = address(0x02020202020202020202);
address public charlie = address(0x03030303030303030303);

function setUp() public {
token1 = new MockToken("Token1", "TK1");
token2 = new MockToken("Token2", "TK2");
token3 = new MockToken("token3", "TK3");

Fee[] memory feeArray = new Fee[](2);
feeArray[0] = Fee(address(token1), 1 ether);
feeArray[1] = Fee(address(token2), 2 ether);

fees = new FeeRegistry(feeArray);

euphrates = new MockEuphrates();
//vm.etch(EUPHRATES, address(euphrates).code);
euphrates.addPool(IERC20(address(token1)));
euphrates.addPool(IERC20(address(token2)));
}

function testRouteWithFee() public {
EuphratesInstructions memory inst = EuphratesInstructions(0, alice);
EuphratesRouter router = new EuphratesRouter(fees, inst, address(euphrates));

token1.transfer(address(router), 5 ether);

vm.prank(bob);
router.route(token1, bob);

assertEq(token1.balanceOf(address(router)), 0);
assertEq(euphrates.shares(0, alice), 4 ether); // (amount - fee)
assertEq(token1.balanceOf(bob), 1 ether); // fee
}

function testRouteWithFeeWithOtherRecipient() public {
EuphratesInstructions memory inst = EuphratesInstructions(0, alice);
EuphratesRouter router = new EuphratesRouter(fees, inst, address(euphrates));

token1.transfer(address(router), 5 ether);

vm.prank(bob);
router.route(token1, charlie);

assertEq(token1.balanceOf(address(router)), 0);
assertEq(euphrates.shares(0, alice), 4 ether); // (amount - fee)
assertEq(token1.balanceOf(charlie), 1 ether); // fee
}

function testRouteWithoutFee() public {
EuphratesInstructions memory inst = EuphratesInstructions(0, alice);
EuphratesRouter router = new EuphratesRouter(fees, inst, address(euphrates));

token1.transfer(address(router), 5 ether);

vm.prank(bob);
router.routeNoFee(token1);

assertEq(token1.balanceOf(address(router)), 0);
assertEq(euphrates.shares(0, alice), 5 ether);
assertEq(token1.balanceOf(bob), 0);
}

function testRouteForNotMatchedToken() public {
EuphratesInstructions memory inst = EuphratesInstructions(0, alice);
EuphratesRouter router = new EuphratesRouter(fees, inst, address(euphrates));

token2.transfer(address(router), 5 ether);

vm.prank(bob);
router.route(token2, bob);

assertEq(token2.balanceOf(address(router)), 0);
assertEq(euphrates.shares(0, alice), 0);
assertEq(token2.balanceOf(alice), 3 ether); // (amount - fee)
assertEq(token2.balanceOf(bob), 2 ether); // fee
}
}
26 changes: 26 additions & 0 deletions test/MockEuphrates.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;

import "euphrates/IStaking.sol";
import "euphrates/Staking.sol";
import "./MockToken.sol";

contract MockEuphrates is Staking, IStakingTo {
using SafeMath for uint256;
using SafeERC20 for IERC20;

function stakeTo(uint256 poolId, uint256 amount, address receiver) public override returns (bool) {
require(amount > 0, "cannot stake 0");
IERC20 shareType = IERC20(address(shareTypes(poolId)));
require(address(shareType) != address(0), "invalid pool");

_totalShares[poolId] = _totalShares[poolId].add(amount);
_shares[poolId][receiver] = _shares[poolId][receiver].add(amount);

shareType.safeTransferFrom(msg.sender, address(this), amount);

emit Stake(receiver, poolId, amount);

return true;
}
}

0 comments on commit 6ce848d

Please sign in to comment.