Skip to content
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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
## Contracts

1. Registry.sol: [0x2b4836d81370e37030727e4dcbd9cc5a772cf43a](https://sepolia.basescan.org/address/0x2b4836d81370e37030727e4dcbd9cc5a772cf43a)
2. Exchange.sol: [0xd9004Edc4bdEB308C4A40fdCbE320bbE5DF4AF77](https://sepolia.basescan.org/address/0xd9004edc4bdeb308c4a40fdcbe320bbe5df4af77)
3. Vault.sol: [0xd580248163CDD5AE3225A700E9f4e7CD525b27b0](https://sepolia.basescan.org/address/0xd580248163cdd5ae3225a700e9f4e7cd525b27b0)
4. XSGD.sol [0xd7260d7063fE5A62A90E6A8DD5A39Ab27A05986B](https://sepolia.basescan.org/token/0xd7260d7063fe5a62a90e6a8dd5a39ab27a05986b)
2. Exchange.sol (V1): [0xd9004Edc4bdEB308C4A40fdCbE320bbE5DF4AF77](https://sepolia.basescan.org/address/0xd9004edc4bdeb308c4a40fdcbe320bbe5df4af77)
3. Exchange.sol (V2): [0x92F5D70ffBE0988DEcD5c1E7A6cb8A048a3Fe75D](https://sepolia.basescan.org/address/0x92F5D70ffBE0988DEcD5c1E7A6cb8A048a3Fe75D)
4. Vault.sol: [0xd580248163CDD5AE3225A700E9f4e7CD525b27b0](https://sepolia.basescan.org/address/0xd580248163cdd5ae3225a700e9f4e7cd525b27b0)
5. XSGD.sol: [0xd7260d7063fE5A62A90E6A8DD5A39Ab27A05986B](https://sepolia.basescan.org/token/0xd7260d7063fe5a62a90e6a8dd5a39ab27a05986b)

## Deployment

Expand Down
3 changes: 2 additions & 1 deletion script/deploy/Exchange.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ contract DeployExchange is Script {

address registryAddress = 0x2b4836d81370e37030727E4DCbd9cC5a772cf43A;
address usdcAddress = 0x036CbD53842c5426634e7929541eC2318f3dCF7e;
address xsgdAddress = 0xd7260d7063fE5A62A90E6A8DD5A39Ab27A05986B;
address vaultAddress = 0xd580248163CDD5AE3225A700E9f4e7CD525b27b0;

Exchange exchange = new Exchange(registryAddress, usdcAddress, vaultAddress);
Exchange exchange = new Exchange(registryAddress, usdcAddress, xsgdAddress, vaultAddress);

console.log("Exchange deployed at:", address(exchange));

Expand Down
138 changes: 134 additions & 4 deletions src/Exchange.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,36 @@ contract Exchange is ReentrancyGuard, Ownable {

Registry public immutable registry;
IERC20 public immutable usdcToken;
IERC20 public immutable xsgdToken;
IERC4626 public immutable vault;

uint256 public fee = 100; // 100 basis points fee (1%)
address public feeCollector;

uint256 public lastUpdateTime;
uint256 public lastPricePerShare;

event Transfer(address indexed from, address indexed to, uint256 amount, string uen);
event FeeUpdated(uint256 newFee);
event FeeCollectorUpdated(address newFeeCollector);
event FeesWithdrawn(address indexed to, uint256 amount);
event VaultDeposit(address indexed merchant, uint256 assets, uint256 shares);
event VaultWithdraw(address indexed merchant, uint256 assets, uint256 shares);

constructor(address _registryAddress, address _usdcAddress, address _vaultAddress) Ownable(msg.sender) {
constructor(address _registryAddress, address _usdcAddress, address _xsgdAddress, address _vaultAddress)
Ownable(msg.sender)
{
require(_registryAddress != address(0), "Invalid registry address");
require(_usdcAddress != address(0), "Invalid USDC address");
require(_xsgdAddress != address(0), "Invalid xSGD address");
require(_vaultAddress != address(0), "Invalid vault address");
registry = Registry(_registryAddress);
usdcToken = IERC20(_usdcAddress);
xsgdToken = IERC20(_xsgdAddress);
feeCollector = address(this);
vault = IERC4626(_vaultAddress);
lastUpdateTime = block.timestamp;
lastPricePerShare = vault.convertToAssets(1e6);
}

/////////////////////////
Expand All @@ -50,7 +60,7 @@ contract Exchange is ReentrancyGuard, Ownable {
* @param _uen Merchant's UEN.
* @param _amount Amount of USDC to transfer to merchant.
*/
function transferToMerchant(string memory _uen, uint256 _amount) external nonReentrant {
function transferUsdcToMerchant(string memory _uen, uint256 _amount) external nonReentrant {
require(_amount > 0, "Amount must be greater than zero");

address merchantWalletAddress = registry.getMerchantByUEN(_uen).wallet_address;
Expand All @@ -67,6 +77,23 @@ contract Exchange is ReentrancyGuard, Ownable {
emit Transfer(msg.sender, merchantWalletAddress, merchantAmount, _uen);
}

function transferXsgdToMerchant(string memory _uen, uint256 _amount) external nonReentrant {
require(_amount > 0, "Amount must be greater than zero");

address merchantWalletAddress = registry.getMerchantByUEN(_uen).wallet_address;
require(merchantWalletAddress != address(0), "Invalid merchant wallet address");

uint256 feeAmount = (_amount * fee) / 10000;
uint256 merchantAmount = _amount - feeAmount;

xsgdToken.safeTransferFrom(msg.sender, merchantWalletAddress, merchantAmount);
if (feeAmount > 0) {
xsgdToken.safeTransferFrom(msg.sender, feeCollector, feeAmount);
}

emit Transfer(msg.sender, merchantWalletAddress, merchantAmount, _uen);
}

/**
* @notice Transfer USDC to vault.
* @param _uen Merchant's UEN.
Expand Down Expand Up @@ -149,16 +176,119 @@ contract Exchange is ReentrancyGuard, Ownable {
}

/**
* @notice Withdraw fees.
* @notice Withdraw USDC fees.
* @param _to Withdrawal address.
* @param _amount Amount of USDC to withdraw.
*/
function withdrawFees(address _to, uint256 _amount) external onlyOwner {
function withdrawUsdcFees(address _to, uint256 _amount) external onlyOwner {
require(_to != address(0), "Invalid withdrawal address");
require(_amount > 0, "Withdrawal amount must be greater than zero");
require(_amount <= usdcToken.balanceOf(address(this)), "Insufficient balance");

usdcToken.safeTransfer(_to, _amount);
emit FeesWithdrawn(_to, _amount);
}

/**
* @notice Withdraw XSGD fees.
* @param _to Withdrawal address.
* @param _amount Amount of XSGD to withdraw.
*/
function withdrawXsgdFees(address _to, uint256 _amount) external onlyOwner {
require(_to != address(0), "Invalid withdrawal address");
require(_amount > 0, "Withdrawal amount must be greater than zero");
require(_amount <= xsgdToken.balanceOf(address(this)), "Insufficient balance");

xsgdToken.safeTransfer(_to, _amount);
emit FeesWithdrawn(_to, _amount);
}

///////////////////
// VAULT HELPERS //
///////////////////

/**
* @notice Get current price per share
* @return Current price of 1 share in terms of assets (USDC)
*/
function getCurrentPricePerShare() public view returns (uint256) {
return vault.convertToAssets(1e6);
}

/**
* @notice Get vault metrics
* @return totalAssets Total assets in vault
* @return totalShares Total shares issued
* @return pricePerShare Current price per share
*/
function getVaultMetrics() public view returns (uint256 totalAssets, uint256 totalShares, uint256 pricePerShare) {
totalAssets = vault.totalAssets();
totalShares = vault.totalSupply();
pricePerShare = getCurrentPricePerShare();
}

/**
* @notice Calculate APY between two price points
* @param startPrice Starting price per share
* @param endPrice Ending price per share
* @param timeElapsedInSeconds Time elapsed between prices in seconds
* @return apy Annual Percentage Yield in basis points (1% = 100)
*/
function calculateAPY(uint256 startPrice, uint256 endPrice, uint256 timeElapsedInSeconds)
public
pure
returns (uint256 apy)
{
require(timeElapsedInSeconds > 0, "Time elapsed must be > 0");
require(startPrice > 0, "Start price must be > 0");

// Calculate yield for the period
uint256 yield = ((endPrice - startPrice) * 1e6) / startPrice;

// Annualize it (multiply by seconds in year and divide by elapsed time)
uint256 secondsInYear = 365 days;
apy = (yield * secondsInYear) / timeElapsedInSeconds;

return apy;
}

/**
* @notice Get current APY based on last update
* @return Current APY in basis points (1% = 100)
*/
function getCurrentAPY() external view returns (uint256) {
uint256 currentPrice = getCurrentPricePerShare();
uint256 timeElapsed = block.timestamp - lastUpdateTime;

return calculateAPY(lastPricePerShare, currentPrice, timeElapsed);
}

/**
* @notice Update the stored price per share
* @dev This can be called periodically to update the reference point for APY calculations
*/
function updatePricePerShare() external {
lastPricePerShare = getCurrentPricePerShare();
lastUpdateTime = block.timestamp;
}

/**
* @notice Get detailed yield information
* @return currentPrice Current price per share
* @return lastPrice Last recorded price per share
* @return timeSinceLastUpdate Seconds since last update
* @return currentAPY Current APY in basis points
*/
function getYieldInfo()
external
view
returns (uint256 currentPrice, uint256 lastPrice, uint256 timeSinceLastUpdate, uint256 currentAPY)
{
currentPrice = getCurrentPricePerShare();
lastPrice = lastPricePerShare;
timeSinceLastUpdate = block.timestamp - lastUpdateTime;
currentAPY = calculateAPY(lastPrice, currentPrice, timeSinceLastUpdate);

return (currentPrice, lastPrice, timeSinceLastUpdate, currentAPY);
}
}
Loading
Loading