diff --git a/Makefile b/Makefile index 0b12a7a..f7d4851 100644 --- a/Makefile +++ b/Makefile @@ -36,15 +36,22 @@ simulate-don: setup-functions: forge script script/FunctionsScript.s.sol:FunctionsScript --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast +deploy-all: + make deploy-madt && \ + make deploy-usdt && \ + make setup-functions && \ + make deploy-vault && \ + make send-request + # Interactions with the DON via the DataProvider contract send-request: - forge script script/Interactions.s.sol:Interactions --sig "sendRequest()" --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast -vvvvv + forge script script/Interactions.s.sol:Interactions --sig "sendRequest()" --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast -vv get-last-response: - forge script script/Interactions.s.sol:Interactions --sig "getLastResponse()" --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast -vvvvv + forge script script/Interactions.s.sol:Interactions --sig "getLastResponse()" --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast -vv get-last-error: - cast call $(CONTRACT_ADDRESS) "getLastError()" --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) + forge script script/Interactions.s.sol:Interactions --sig "getLastError()" --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast -vv get-last-request-id: cast call $(CONTRACT_ADDRESS) "getLastRequestId()" --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) @@ -53,17 +60,24 @@ get-last-request-id: deploy-madt: forge script script/DeployMADT.s.sol:DeployMADT --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast +deploy-usdt: + forge script script/DeployMockUSDT.s.sol:DeployMockUSDT --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast + deploy-vault: - forge script script/DeployVault.s.sol:DeployVault --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast -vvvvv + forge script script/DeployVault.s.sol:DeployVault --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast -vv # Interactions with the Vault contract # Deposit collateral deposit-collateral: - forge script script/VaultInteractions.s.sol:VaultInteractions --sig "depositCollateral()" --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast -vvvvv + forge script script/VaultInteractions.s.sol:VaultInteractions --sig "depositCollateral()" --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast -vv # Redeem collateral redeem-collateral: - forge script script/VaultInteractions.s.sol:VaultInteractions --sig "redeemCollateral(uint256)" --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast -vvvvv + forge script script/VaultInteractions.s.sol:VaultInteractions --sig "redeemCollateral(uint256)" $(AMOUNT) --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast -vv + +# Rebase +rebase: + forge script script/VaultInteractions.s.sol:VaultInteractions --sig "rebase()" --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast -vv NETWORK_ARGS := --rpc-url http://localhost:8545 --private-key $(DEFAULT_ANVIL_KEY) --broadcast diff --git a/don-simulator/package-lock.json b/don-simulator/package-lock.json index 913a0e0..86f9167 100644 --- a/don-simulator/package-lock.json +++ b/don-simulator/package-lock.json @@ -6,6 +6,7 @@ "": { "dependencies": { "cbor": "^10.0.3", + "dotenv": "^16.4.7", "ethers": "^5.7.2" }, "devDependencies": { @@ -759,6 +760,18 @@ "node": ">=18" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/elliptic": { "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", diff --git a/don-simulator/package.json b/don-simulator/package.json index d720980..dd4910d 100644 --- a/don-simulator/package.json +++ b/don-simulator/package.json @@ -4,6 +4,7 @@ }, "dependencies": { "cbor": "^10.0.3", + "dotenv": "^16.4.7", "ethers": "^5.7.2" } } diff --git a/don-simulator/src/config.js b/don-simulator/src/config.js new file mode 100644 index 0000000..3cc8137 --- /dev/null +++ b/don-simulator/src/config.js @@ -0,0 +1,36 @@ +require("dotenv").config(); +const Location = { + Inline: 0, + Remote: 1, + DONHosted: 2, +}; + +const CodeLanguage = { + JavaScript: 0, +}; + +const ReturnType = { + uint: "uint256", + uint256: "uint256", + int: "int256", + int256: "int256", + string: "string", + bytes: "bytes", +}; + +// Configure the request by setting the fields below +const requestConfig = { + // Location of source code (only Inline is currently supported) + codeLocation: Location.Inline, + // Optional. Secrets can be accessed within the source code with `secrets.varName` (ie: secrets.apiKey). The secrets object can only contain string values. + secrets: { + exchangeRateApiKey: process.env.EXCH_RATE_KEY ?? "", + currencyApiKey: process.env.CURRENCY_KEY ?? "", + }, + // Code language (only JavaScript is currently supported) + codeLanguage: CodeLanguage.JavaScript, + // Expected type of the returned value + expectedReturnType: ReturnType.uint256, +}; + +module.exports = requestConfig; diff --git a/don-simulator/src/localFunctionsTestnet.ts b/don-simulator/src/localFunctionsTestnet.ts index 376be67..5d65a68 100644 --- a/don-simulator/src/localFunctionsTestnet.ts +++ b/don-simulator/src/localFunctionsTestnet.ts @@ -31,6 +31,7 @@ import type { FunctionsContracts, RequestEventData, } from "./types"; +import path from "path"; export const startLocalFunctionsTestnet = async ( coordinatorAddress?: string, @@ -39,6 +40,23 @@ export const startLocalFunctionsTestnet = async ( ): Promise => { const provider = new providers.JsonRpcProvider(`http://localhost:${port}`); + // Add error handler for provider disconnection + provider.on("error", async (error) => { + console.log("Provider error detected, shutting down testnet..."); + // Clean up event listeners and close connections + await close(); + process.exit(1); + }); + + // Add network change handler + provider.on("network", async (newNetwork, oldNetwork) => { + if (oldNetwork) { + console.log("Network connection lost, shutting down testnet..."); + await close(); + process.exit(1); + } + }); + const admin = new Wallet( "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", provider @@ -100,9 +118,14 @@ export const startLocalFunctionsTestnet = async ( const getFunds: GetFunds = async () => {}; const close = async (): Promise => { + // Remove all event listeners contracts.functionsMockCoordinatorContract.removeAllListeners( "OracleRequest" ); + provider.removeAllListeners(); + + // Optional: Add any additional cleanup needed + console.log("Local Functions testnet shut down successfully"); }; return { @@ -494,8 +517,15 @@ if (require.main === module) { try { const args = process.argv.slice(2); const coordinatorAddress = args[0]; + const configPath = path.join( + process.cwd(), + "don-simulator/src/config.js" + ); console.log("Starting local Functions testnet..."); - const testnet = await startLocalFunctionsTestnet(coordinatorAddress); + const testnet = await startLocalFunctionsTestnet( + coordinatorAddress, + configPath + ); console.log("Local Functions testnet started successfully"); // Keep the process running diff --git a/don-simulator/src/source/source.js b/don-simulator/src/source/source.js index 00dec2e..0b71796 100644 --- a/don-simulator/src/source/source.js +++ b/don-simulator/src/source/source.js @@ -1,2 +1,51 @@ -return Functions.encodeUint256(0.1 * 100); - \ No newline at end of file +// // Discontinued because of API changes. +// // const bkamAPI = Functions.makeHttpRequest({ +// // url: `https://api.centralbankofmorocco.ma/cours/Version1/api/CoursVirement?libDevise=USD`, +// // headers: { "Ocp-Apim-Subscription-Key": secrets.BKAM_KEY }, +// // }) + +// if (secrets.exchangeRateApiKey.length === 0) { +// throw Error("No valid EXCH_RATE_KEY was supplied"); +// } + +// if (secrets.currencyApiKey.length === 0) { +// throw Error("No valid CURRENCY_KEY was supplied"); +// } + +// const exchRateAPI = Functions.makeHttpRequest({ +// url: `https://v6.exchangerate-api.com/v6/${secrets.exchangeRateApiKey}/pair/MAD/USD`, +// }); + +// const currencyAPI = Functions.makeHttpRequest({ +// url: `https://api.currencyapi.com/v3/latest?apikey=${secrets.currencyApiKey}¤cies=USD&base_currency=MAD`, +// }); + +// const [exchRateAPIResponse, currencyAPIResponse] = await Promise.all([ +// exchRateAPI, +// currencyAPI, +// ]); + +// const prices = []; +// if (!exchRateAPIResponse.error) { +// prices.push(exchRateAPIResponse.data.conversion_rate); +// } else { +// console.log("ExchangeRateAPI Error"); +// throw Error(JSON.stringify(exchRateAPIResponse)); +// } +// if (!currencyAPIResponse.error) { +// prices.push(currencyAPIResponse.data.data.USD.value); +// } else { +// console.log("CurrencyAPI Error"); +// throw Error(JSON.stringify(currencyAPIResponse)); +// } + +// if (prices.length < 2) { +// // If an error is thrown, it will be returned back to the smart contract +// throw Error("More than 1 API failed"); +// } + +// const medianRate = prices.sort((a, b) => a - b)[Math.round(prices.length / 2)]; +// console.log(`Median MAD rate: $${medianRate.toFixed(2)}`); + +// return Functions.encodeUint256(Math.round(medianRate * 100)); +return Functions.encodeUint256(11); diff --git a/don-simulator/src/test.js b/don-simulator/src/test.js new file mode 100644 index 0000000..09fda35 --- /dev/null +++ b/don-simulator/src/test.js @@ -0,0 +1,3 @@ +require("dotenv").config(); + +console.log(process.env.EXCH_RATE_KEY); \ No newline at end of file diff --git a/script/DeployMockUSDT.s.sol b/script/DeployMockUSDT.s.sol new file mode 100644 index 0000000..4ecdd14 --- /dev/null +++ b/script/DeployMockUSDT.s.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Script} from "forge-std/Script.sol"; +import {MockUSDT} from "../src/MockUSDT.sol"; + +contract DeployMockUSDT is Script { + function run() public { + vm.startBroadcast(vm.envUint("PRIVATE_KEY")); + new MockUSDT(); + vm.stopBroadcast(); + } +} diff --git a/script/DeployVault.s.sol b/script/DeployVault.s.sol index 51d9756..8d02fde 100644 --- a/script/DeployVault.s.sol +++ b/script/DeployVault.s.sol @@ -11,18 +11,18 @@ import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {DevOpsTools} from "lib/foundry-devops/src/DevOpsTools.sol"; contract DeployVault is Script, HelperConfig { - IDataProvider public dataProvider; - address contractAddress = DevOpsTools.get_most_recent_deployment("DataProvider", block.chainid); - address MADTAddress = DevOpsTools.get_most_recent_deployment("MADT", block.chainid); + function run() public { + address contractAddress = DevOpsTools.get_most_recent_deployment("DataProvider", block.chainid); + address MADTAddress = DevOpsTools.get_most_recent_deployment("MADT", block.chainid); - MADT public madt = MADT(MADTAddress); + MADT madt = MADT(MADTAddress); - function run() public { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); - dataProvider = IDataProvider(contractAddress); + console.log("Deploying Vault.."); + IDataProvider dataProvider = IDataProvider(contractAddress); IERC20 usdt = IERC20(getNetworkConfig().usdToken); Vault vault = new Vault(dataProvider, madt, usdt); - // madt.setVault(address(vault)); + madt.setVault(address(vault)); vm.stopBroadcast(); } } diff --git a/script/HelperConfig.s.sol b/script/HelperConfig.s.sol index e9e0833..9799f25 100644 --- a/script/HelperConfig.s.sol +++ b/script/HelperConfig.s.sol @@ -5,12 +5,14 @@ import {Script} from "forge-std/Script.sol"; import {console} from "forge-std/console.sol"; import {MockUSDT} from "../src/MockUSDT.sol"; +import {DevOpsTools} from "lib/foundry-devops/src/DevOpsTools.sol"; contract HelperConfig is Script { // If we are on a local chain, we deploy the mock contract // Otherwise, we fetch the existing address from the live network uint256 public constant ETH_SEPOLIA_CHAIN_ID = 11155111; + address public usdtAddress = DevOpsTools.get_most_recent_deployment("MockUSDT", block.chainid); struct NetworkConfig { address linkToken; @@ -46,11 +48,9 @@ contract HelperConfig is Script { } function getAnvilEthConfig() public returns (NetworkConfig memory anvilConfig) { - MockUSDT usdt = new MockUSDT(); - anvilConfig = NetworkConfig({ linkToken: vm.envAddress("LINK_TOKEN_ADDRESS"), - usdToken: address(usdt), + usdToken: usdtAddress, subscriptionId: 1, donId: stringToBytes32(vm.envString("DON_ID")), router: vm.envAddress("FUNCTIONS_ROUTER_ADDRESS"), diff --git a/script/Interactions.s.sol b/script/Interactions.s.sol index bbac48b..3c6da79 100644 --- a/script/Interactions.s.sol +++ b/script/Interactions.s.sol @@ -15,9 +15,9 @@ contract Interactions is Script, HelperConfig { function getLastResponse() public returns (uint256) { vm.startBroadcast(vm.envUint("PRIVATE_KEY")); dataProvider = IDataProvider(contractAddress); - bytes memory response = dataProvider.getLastResponse(); - return abi.decode(response, (uint256)); + uint256 madValue = dataProvider.getMADValueInUSD(); vm.stopBroadcast(); + return madValue; } function sendRequest() public { diff --git a/script/VaultInteractions.s.sol b/script/VaultInteractions.s.sol index 9d80ba5..4655c22 100644 --- a/script/VaultInteractions.s.sol +++ b/script/VaultInteractions.s.sol @@ -7,16 +7,31 @@ import {IDataProvider} from "../src/interfaces/IDataProvider.sol"; import {FunctionsScript} from "./FunctionsScript.s.sol"; import {Vault} from "../src/Vault.sol"; import {DevOpsTools} from "lib/foundry-devops/src/DevOpsTools.sol"; +import {MockUSDT} from "../src/MockUSDT.sol"; contract VaultInteractions is Script, HelperConfig { IDataProvider public dataProvider; address contractAddress = DevOpsTools.get_most_recent_deployment("Vault", block.chainid); Vault public vault = Vault(contractAddress); + MockUSDT public usdt = MockUSDT(usdtAddress); function depositCollateral() public { vm.startBroadcast(); - vault.depositCollateral(100); + usdt.approve(contractAddress, 100000000000000000000000000000); + vault.depositCollateral(12500); + vm.stopBroadcast(); + } + + function redeemCollateral(uint256 amountInUsd) public { + vm.startBroadcast(); + vault.redeemCollateral(amountInUsd); + vm.stopBroadcast(); + } + + function rebase() public { + vm.startBroadcast(); + vault.rebase(); vm.stopBroadcast(); } } diff --git a/src/MADT.sol b/src/MADT.sol index 0c876f5..16f465c 100644 --- a/src/MADT.sol +++ b/src/MADT.sol @@ -10,6 +10,7 @@ contract MADT is ERC20, Ownable { error MADT__InvalidVaultAddress(); address public vault; + uint256 public rebaseFactor = 1e18; constructor() ERC20("Tokenized MAD", "MADT") Ownable(msg.sender) {} @@ -30,4 +31,34 @@ contract MADT is ERC20, Ownable { function burn(address from, uint256 amount) public onlyVault { _burn(from, amount); } + + function balanceOf(address account) public view override returns (uint256) { + return (super.balanceOf(account) * rebaseFactor) / 1e18; + } + + function transfer(address to, uint256 amount) public override returns (bool) { + uint256 scaledAmount = (amount * 1e18) / rebaseFactor; + return super.transfer(to, scaledAmount); + } + + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + uint256 scaledAmount = (amount * 1e18) / rebaseFactor; + return super.transferFrom(from, to, scaledAmount); + } + + function rebase(uint256 madtToUSDTPrice) public onlyVault { + require(madtToUSDTPrice > 0, "Invalid MADT to USDT price"); + + // uint256 oldRebaseFactor = rebaseFactor; + uint256 scalingFactor = 100; + + // Calculate the new rebase factor based on the ratio + uint256 newRebaseFactor = (rebaseFactor * madtToUSDTPrice) / scalingFactor; + rebaseFactor = newRebaseFactor; + // emit Rebase(oldRebaseFactor, rebaseFactor); + } + + function decimals() public pure override returns (uint8) { + return 6; + } } diff --git a/src/MockUSDT.sol b/src/MockUSDT.sol index be41a34..5b13486 100644 --- a/src/MockUSDT.sol +++ b/src/MockUSDT.sol @@ -9,7 +9,7 @@ contract MockUSDT is ERC20 { uint256 private constant INITIAL_SUPPLY = 1000e18; constructor() ERC20("Mock USDT", "USDT") { - _mint(msg.sender, 100000000000000000000); + _mint(msg.sender, 100000000000); } function decimals() public pure override returns (uint8) { diff --git a/src/Vault.sol b/src/Vault.sol index f7a1a18..48f6088 100644 --- a/src/Vault.sol +++ b/src/Vault.sol @@ -37,7 +37,7 @@ contract Vault { function depositCollateral(uint256 amountInUsd) public payable returns (bool) { uint256 madValue = dataProvider.getMADValueInUSD(); - uint256 madAmount = (amountInUsd * 1e18) / madValue; + uint256 madAmount = (amountInUsd * (10 ** 8)) / madValue; bool success = usdt.transferFrom(msg.sender, address(this), amountInUsd * (10 ** 6)); if (!success) revert Vault__TransferFailed(); @@ -56,22 +56,27 @@ contract Vault { function redeemCollateral(uint256 amountInUsd) public payable returns (bool) { uint256 madValue = dataProvider.getMADValueInUSD(); - uint256 madAmount = (amountInUsd * 1e18) / madValue; + uint256 madAmount = (amountInUsd * (10 ** 8)) / madValue; if (madt.balanceOf(msg.sender) < madAmount) { revert Vault__UserInsufficientBalance(); } - if (usdt.balanceOf(address(this)) < amountInUsd * 1e6) { + if (usdt.balanceOf(address(this)) < amountInUsd * (10 ** 6)) { revert Vault__VaultInsufficientBalance(); } madt.burn(msg.sender, madAmount); - bool success = usdt.transfer(msg.sender, amountInUsd * 1e6); + bool success = usdt.transfer(msg.sender, amountInUsd * (10 ** 6)); if (!success) revert Vault__TransferFailed(); emit CollateralRedeemed(msg.sender, amountInUsd); return success; } + + function rebase() public { + uint256 madValue = dataProvider.getMADValueInUSD(); + madt.rebase(madValue); + } }