Skip to content
This repository was archived by the owner on Jul 7, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,18 @@ jobs:

- name: Run Forge fmt
run: |
cd eth-contracts
forge fmt --check
id: fmt

- name: Run Forge build
run: |
cd eth-contracts
forge build --sizes
id: build

- name: Run Forge tests
run: |
cd eth-contracts
forge test -vvv
id: test
15 changes: 15 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
[submodule "eth-contracts/lib/forge-std"]
path = eth-contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/openzeppelin-foundry-upgrades"]
path = lib/openzeppelin-foundry-upgrades
url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
[submodule "lib/openzeppelin-contracts-upgradeable"]
path = lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
[submodule "eth-contracts/lib/openzeppelin-foundry-upgrades"]
path = eth-contracts/lib/openzeppelin-foundry-upgrades
url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
[submodule "eth-contracts/lib/openzeppelin-contracts-upgradeable"]
path = eth-contracts/lib/openzeppelin-contracts-upgradeable
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
5 changes: 5 additions & 0 deletions .idea/contracts.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
23.7.0
1 change: 1 addition & 0 deletions eth-contracts/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
save-exact=true
15 changes: 0 additions & 15 deletions eth-contracts/README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
## Foundry

**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**

Foundry consists of:

- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.

## Documentation

https://book.getfoundry.sh/

## Usage

### Build

```shell
Expand Down
1 change: 1 addition & 0 deletions eth-contracts/lib/openzeppelin-foundry-upgrades
7 changes: 6 additions & 1 deletion eth-contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,10 @@
"format": "forge fmt"
},
"author": "Abdulla Faraz <[email protected]>",
"license": "MIT"
"license": "MIT",
"dependencies": {
"@uniswap/sdk-core": "7.5.0",
"@uniswap/v3-core": "1.0.1",
"@uniswap/v3-periphery": "1.4.4"
}
}
5 changes: 5 additions & 0 deletions eth-contracts/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
@uniswap/v3-periphery/=node_modules/@uniswap/v3-periphery/
@uniswap/v3-core/=node_modules/@uniswap/v3-core/
@uniswap/v3-sdk/=node_modules/@uniswap/v3-sdk/
19 changes: 0 additions & 19 deletions eth-contracts/script/Counter.s.sol

This file was deleted.

188 changes: 188 additions & 0 deletions eth-contracts/src/AbstractMultiWalletAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@uniswap/v3-periphery/contracts/libraries/TransferHelper.sol";
import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import "./lib/AddressSet.sol";

/*
* AbstractMultiWalletAccount contract
*
* This contract is a simple contract that allows a user to deposit funds to an
* operator held account and withdraw funds from it.
*
*/
abstract contract AbstractMultiWalletAccount {
/*
* The operator of the account (the fund manager if it's a fund)
* Operator should not be able to withdraw funds from account.
* Operator can swap funds between account after verifying signature.
* Operator can swap tokens to other tokens if permission is given by user.
*/
address public operator;
/*
* The base token of the account (the token in which the account is denominated)
* All funds in the account are in base token.
*/
address public baseToken;

/*
* The set of tokens held in this contract
*/
using AddressSet for AddressSet.Set;

AddressSet.Set tokenSet;
/*
* The set of depositor who have funds in this contract
*/
AddressSet.Set depositorSet;

/*
* This mapping stores user balances for each token (user => token => amount)
*/
mapping(address => mapping(address => uint256)) public wallets;

/*
* This mapping stores total balances for each token
*/
mapping(address => uint256) public contractTokenBalances;

/*
* this struct is used to pass parameters to swapOnUniswap function
*/
struct SwapOnUniswapParams {
address tokenAddressIn;
address tokenAddressOut;
address swapRouter;
uint24 poolFee;
uint256 amountIn;
uint256 deadline;
uint256 amountOutMinimum;
uint160 sqrtPriceLimitX96;
uint256 estimatedGasFees; // gas fee in base currency
}

event DepositEvent(address from, address tokenAddress, uint256 amountReceived);
event WithdrawEvent(address to, address tokenAddress, uint256 amountSent);
event SwapSucceeded(uint256 amountOut, address tokenOut, uint256 amountIn, address tokenIn, uint24 poolFee);

constructor(address _operator, address _baseToken) {
operator = _operator;
baseToken = _baseToken;
tokenSet.insert(_baseToken);
}

function deposit(uint256 amount) external {
address depositor = msg.sender;

IERC20 baseTokenContract = IERC20(baseToken);

// check allowance
uint256 allowance = baseTokenContract.allowance(msg.sender, address(this));
require(allowance >= amount, "Allowance is not enough");

// transfer
bool sent = baseTokenContract.transferFrom(depositor, address(this), amount);
require(sent, "Failed to deposit funds");

// update balance
wallets[depositor][baseToken] += amount;
contractTokenBalances[baseToken] += amount;
emit DepositEvent(msg.sender, baseToken, amount);
}

function withdraw(address tokenAddress) external {
address withdrawer = msg.sender;
uint256 balance = wallets[withdrawer][tokenAddress];
require(balance > 0, "Insufficient balance");

// update balance
wallets[withdrawer][tokenAddress] = 0;
contractTokenBalances[tokenAddress] -= balance;

IERC20 tokenContract = IERC20(tokenAddress);

// transfer
bool sent = tokenContract.transfer(withdrawer, balance);
require(sent, "Failed to withdraw funds");

emit WithdrawEvent(withdrawer, tokenAddress, balance);
}

function checkBalance(address user, address tokenAddress) public view returns (uint256) {
return wallets[user][tokenAddress];
}

function swapOnUniswap(SwapOnUniswapParams calldata swapParams) external onlyOperator {
require(tokenSet.contains(swapParams.tokenAddressIn), "Token not supported");
require(contractTokenBalances[swapParams.tokenAddressIn] >= swapParams.amountIn, "Insufficient balance");

ISwapRouter swapRouter = ISwapRouter(swapParams.swapRouter);

uint256 amountInAfterFee = swapParams.amountIn - swapParams.estimatedGasFees;

ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
tokenIn: swapParams.tokenAddressIn,
tokenOut: swapParams.tokenAddressOut,
fee: swapParams.poolFee,
recipient: address(this),
deadline: block.timestamp + swapParams.deadline,
amountIn: amountInAfterFee,
amountOutMinimum: swapParams.amountOutMinimum,
sqrtPriceLimitX96: swapParams.sqrtPriceLimitX96
});

uint256 amountOut = swapRouter.exactInputSingle(params);
require(amountOut >= swapParams.amountOutMinimum, "Uniswap swap failed");

uint256 depositorCount = depositorSet.size();
address[] memory depositors = new address[](depositorCount);
depositors = depositorSet.values();

uint256 contractTokenBalance = contractTokenBalances[swapParams.tokenAddressIn];

for (uint256 i = 0; i < depositorCount; i++) {
address depositor = depositors[i];
uint256 balance = wallets[depositor][swapParams.tokenAddressIn];
if (balance > 0) {
// deduct tokenIn from users who have tokenIn
uint256 depositorShare = (balance / contractTokenBalance);
wallets[depositor][swapParams.tokenAddressIn] -= depositorShare * swapParams.amountIn;
// update tokenOut balance for users
wallets[depositor][swapParams.tokenAddressOut] += depositorShare * amountOut;
}
}
// update contractTokenBalances
contractTokenBalances[swapParams.tokenAddressIn] -= swapParams.amountIn;

// add tokenOut to tokenSet
tokenSet.insert(swapParams.tokenAddressOut);

// update contractTokenBalances
contractTokenBalances[swapParams.tokenAddressOut] += amountOut;

// emit event
emit SwapSucceeded(
amountOut, swapParams.tokenAddressOut, swapParams.amountIn, swapParams.tokenAddressIn, swapParams.poolFee
);
}

modifier onlyOperator() {
require(msg.sender == operator, "Only operator can call this function");
_;
}

function receiveEthBalance() external payable onlyOperator {}

receive() external payable onlyOperator {}

fallback() external payable onlyOperator {}

function withdrawEthBalance() external onlyOperator {
uint256 balance = address(this).balance;
address caller = msg.sender;
(bool sent, bytes memory data) = caller.call{value: balance}("");
require(sent, "Failed to send Ether");
}
}
14 changes: 0 additions & 14 deletions eth-contracts/src/Counter.sol

This file was deleted.

8 changes: 8 additions & 0 deletions eth-contracts/src/MultiWalletAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {AbstractMultiWalletAccount} from "./AbstractMultiWalletAccount.sol";

contract MultiWalletAccount is AbstractMultiWalletAccount {
constructor(address _operator, address _baseToken) AbstractMultiWalletAccount(_operator, _baseToken) {}
}
45 changes: 45 additions & 0 deletions eth-contracts/src/lib/AddressSet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

// Copyright (c), 2019 Rob Hitchens

library AddressSet {
struct Set {
mapping(address => uint256) keyPointers;
address[] keyList;
}

function insert(Set storage self, address key) internal {
require(key != address(0), "UnorderedKeySet(100) - Key cannot be 0x0");
require(!contains(self, key), "UnorderedAddressSet(101) - Address (key) already exists in the set.");
self.keyList.push(key);
self.keyPointers[key] = self.keyList.length - 1;
}

function remove(Set storage self, address key) internal {
require(contains(self, key), "UnorderedKeySet(102) - Address (key) does not exist in the set.");
address keyToMove = self.keyList[size(self) - 1];
uint256 rowToReplace = self.keyPointers[key];
self.keyPointers[keyToMove] = rowToReplace;
self.keyList[rowToReplace] = keyToMove;
delete self.keyPointers[key];
self.keyList.pop();
}

function size(Set storage self) internal view returns (uint256) {
return (self.keyList.length);
}

function contains(Set storage self, address key) internal view returns (bool) {
if (self.keyList.length == 0) return false;
return self.keyList[self.keyPointers[key]] == key;
}

function keyAtIndex(Set storage self, uint256 index) internal view returns (address) {
return self.keyList[index];
}

function values(Set storage self) internal view returns (address[] memory) {
return self.keyList;
}
}
Loading