diff --git a/.vscode/settings.json b/.vscode/settings.json index 5a23bb8..4b61eb9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,8 @@ { - "solidity.packageDefaultDependenciesContractsDirectory": "pkg/contracts/src", - "solidity.packageDefaultDependenciesDirectory": "lib", "[solidity]": { "editor.defaultFormatter": "JuanBlanco.solidity", "editor.formatOnSave": true }, "solidity.formatter": "forge", + "solidity.compileUsingRemoteVersion": "v0.4.24", } diff --git a/pkg/abc-template/.gitignore b/pkg/abc-template/.gitignore new file mode 100644 index 0000000..aaa61ca --- /dev/null +++ b/pkg/abc-template/.gitignore @@ -0,0 +1,122 @@ +# vscode +.vscode + +# hardhat +artifacts +cache +deployments +node_modules + + +cache/ +artifacts/ + +coverage* +typechain/ + +.vscode/* +!.vscode/settings.json.default +!.vscode/launch.json.default +!.vscode/extensions.json.default + +node_modules/ +.env + +.yalc +yalc.lock + +contractsInfo.json +deployments/hardhat +deployments/localhost + +# don't push the environment vars! +.env + +# Built application files +.DS* +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml +.idea + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +node_modules +node_modules diff --git a/pkg/abc-template/README.md b/pkg/abc-template/README.md new file mode 100644 index 0000000..28a9930 --- /dev/null +++ b/pkg/abc-template/README.md @@ -0,0 +1,16 @@ +# ABC Template + +This package contains the smart contracts needed to deploy the ABC Template. + +## Usage + +Edit hardhat.config.js to add the API key for your Alchemy account and Etherscan API key. + +``` +$ bun hardhat run scripts/deploy.js --network +``` + +## Deployed Contracts + +The contracts are deployed on the following networks: +- Optimism: [0x65e4D3410CF41eAa906Ff3685D537a232144A559](https://optimistic.etherscan.io/address/0x65e4D3410CF41eAa906Ff3685D537a232144A559#code) \ No newline at end of file diff --git a/pkg/abc-template/contracts/AbcBaseTemplate.sol b/pkg/abc-template/contracts/AbcBaseTemplate.sol new file mode 100644 index 0000000..f18d358 --- /dev/null +++ b/pkg/abc-template/contracts/AbcBaseTemplate.sol @@ -0,0 +1,100 @@ +pragma solidity 0.4.24; + +import "@aragon/os/contracts/common/IsContract.sol"; +import "@aragon/os/contracts/acl/ACL.sol"; +import "@aragon/os/contracts/kernel/Kernel.sol"; +import "@aragon/os/contracts/common/Uint256Helpers.sol"; +import "@aragon/apps-token-manager/contracts/TokenManager.sol"; +import "@aragon/apps-vault/contracts/Vault.sol"; +import "@aragon/templates-shared/contracts/BaseTemplate.sol"; +import "./interfaces/IAugmentedBondingCurve.sol"; + +contract AbcBaseTemplate is BaseTemplate { + using Uint256Helpers for uint256; + + /* Hardcoded constant to save gas + * bytes32 constant internal ABC_APP_ID = keccak256(abi.encodePacked(apmNamehash("open"), keccak256("augmented-bonding-curve"))); // augmented-bonding-curve.open.aragonpm.eth + */ + bytes32 internal constant ABC_APP_ID = 0x952fcbadf8d7288f1a8b47ed7ee931702318b527558093398674db0c93e3a75b; + uint256 private constant VIRTUAL_SUPPLY = 0; + uint256 private constant VIRTUAL_BALANCE = 0; + string private constant ERROR_FORMULA_IS_NOT_CONTRACT = "TEMPLATE_FORMULA_IS_NOT_CONTRACT"; + string private constant ERROR_CANNOT_CAST_VALUE_TO_TYPE = "TEMPLATE_CANNOT_CAST_VALUE_TO_TYPE"; + address private formula; + + constructor(address _formula) public { + require(isContract(_formula), ERROR_FORMULA_IS_NOT_CONTRACT); + formula = _formula; + } + + function _installAbcApp( + Kernel _dao, + TokenManager _tokenManager, + Vault _reserve, + address _beneficiary, + uint256 _entryTribute, + uint256 _exitTribute + ) internal returns (IAugmentedBondingCurve) { + bytes memory initializeData = abi.encodeWithSelector( + IAugmentedBondingCurve(0).initialize.selector, + _tokenManager, + formula, + _reserve, + _beneficiary, + _entryTribute, + _exitTribute + ); + return IAugmentedBondingCurve(_installNonDefaultApp(_dao, ABC_APP_ID, initializeData)); + } + + function _configureAbcApp(ACL _acl, IAugmentedBondingCurve _abc, address _collateralToken, uint32 _reserveRatio) + internal + { + _createPermissionForTemplate(_acl, _abc, _abc.MANAGE_COLLATERAL_TOKEN_ROLE()); + _abc.addCollateralToken(_collateralToken, VIRTUAL_SUPPLY, VIRTUAL_BALANCE, _reserveRatio); + _removePermissionFromTemplate(_acl, _abc, _abc.MANAGE_COLLATERAL_TOKEN_ROLE()); + } + + function _createAbcAndReservePermissions( + ACL _acl, + IAugmentedBondingCurve _abc, + address _collateralManager, + address _permissionsManager + ) internal { + Vault _reserve = _abc.reserve(); + _acl.createPermission(_collateralManager, _abc, _abc.MANAGE_COLLATERAL_TOKEN_ROLE(), _permissionsManager); + _acl.createPermission(_acl.ANY_ENTITY(), _abc, _abc.MAKE_BUY_ORDER_ROLE(), _permissionsManager); + _acl.createPermission(_acl.ANY_ENTITY(), _abc, _abc.MAKE_SELL_ORDER_ROLE(), _permissionsManager); + _acl.createPermission(_abc, _reserve, _reserve.TRANSFER_ROLE(), _permissionsManager); + } + + function _unwrapAbcSettings(uint256[5] memory _abcSettings) + internal + pure + returns ( + uint256 entryTribute, + uint256 exitTribute, + address collateralToken, + uint32 reserveRatio, + uint256 initialBalance + ) + { + entryTribute = _abcSettings[0]; + exitTribute = _abcSettings[1]; + collateralToken = _toAddress(_abcSettings[2]); + reserveRatio = _toUint32(_abcSettings[3]); + initialBalance = _abcSettings[4]; + } + + /* HELPERS */ + + function _toAddress(uint256 _value) private pure returns (address) { + require(_value <= uint160(-1), ERROR_CANNOT_CAST_VALUE_TO_TYPE); + return address(_value); + } + + function _toUint32(uint256 _value) private pure returns (uint32) { + require(_value <= uint32(-1), ERROR_CANNOT_CAST_VALUE_TO_TYPE); + return uint32(_value); + } +} diff --git a/pkg/abc-template/contracts/AbcTemplate.sol b/pkg/abc-template/contracts/AbcTemplate.sol new file mode 100644 index 0000000..d73bf6f --- /dev/null +++ b/pkg/abc-template/contracts/AbcTemplate.sol @@ -0,0 +1,178 @@ +pragma solidity 0.4.24; + +import "@aragon/templates-shared/contracts/TokenCache.sol"; +import "@aragon/os/contracts/common/SafeERC20.sol"; +import "./AbcBaseTemplate.sol"; + +contract AbcTemplate is AbcBaseTemplate, TokenCache { + using SafeERC20 for ERC20; + + string private constant ERROR_EMPTY_HOLDERS = "ABC_EMPTY_HOLDERS"; + string private constant ERROR_BAD_HOLDERS_STAKES_LEN = "ABC_BAD_HOLDERS_STAKES_LEN"; + string private constant ERROR_BAD_VOTE_SETTINGS = "ABC_BAD_VOTE_SETTINGS"; + string private constant ERROR_BAD_ABC_SETTINGS = "ABC_BAD_ABC_SETTINGS"; + string private constant ERROR_FUNDS_NOT_TRANSFERRED = "ABC_FUNDS_NOT_TRANSFERRED"; + + bool private constant TOKEN_TRANSFERABLE = true; + uint8 private constant TOKEN_DECIMALS = uint8(18); + uint256 private constant TOKEN_MAX_PER_ACCOUNT = uint256(0); + uint64 private constant DEFAULT_FINANCE_PERIOD = uint64(30 days); + + constructor( + DAOFactory _daoFactory, + ENS _ens, + MiniMeTokenFactory _miniMeFactory, + IFIFSResolvingRegistrar _aragonID, + address _abcFormula + ) public BaseTemplate(_daoFactory, _ens, _miniMeFactory, _aragonID) AbcBaseTemplate(_abcFormula) { + _ensureAragonIdIsValid(_aragonID); + _ensureMiniMeFactoryIsValid(_miniMeFactory); + } + + /** + * @dev Create a new MiniMe token and deploy a ABC DAO. + * @param _tokenName String with the name for the token used by share holders in the organization + * @param _tokenSymbol String with the symbol for the token used by share holders in the organization + * @param _id String with the name for org, will assign `[id].aragonid.eth` + * @param _holders Array of token holder addresses + * @param _stakes Array of token stakes for holders (token has 18 decimals, multiply token amount `* 10^18`) + * @param _votingSettings Array of [supportRequired, minAcceptanceQuorum, voteDuration] to set up the voting app of the organization + * @param _abcSettings Array of [uint256 entryTribute, uint256 exitTribute, address collateralToken, uint32 reserveRatio, uint256 initialBalance] + */ + function newTokenAndInstance( + string _tokenName, + string _tokenSymbol, + string _id, + address[] _holders, + uint256[] _stakes, + uint64[3] _votingSettings, + uint256[5] _abcSettings + ) external { + newToken(_tokenName, _tokenSymbol); + newInstance(_id, _holders, _stakes, _votingSettings, _abcSettings); + } + + /** + * @dev Create a new MiniMe token and cache it for the user + * @param _name String with the name for the token used by share holders in the organization + * @param _symbol String with the symbol for the token used by share holders in the organization + */ + function newToken(string memory _name, string memory _symbol) public returns (MiniMeToken) { + MiniMeToken token = _createToken(_name, _symbol, TOKEN_DECIMALS); + _cacheToken(token, msg.sender); + return token; + } + + /** + * @dev Deploy a ABC DAO using a previously cached MiniMe token + * @param _id String with the name for org, will assign `[id].aragonid.eth` + * @param _holders Array of token holder addresses + * @param _stakes Array of token stakes for holders (token has 18 decimals, multiply token amount `* 10^18`) + * @param _votingSettings Array of [supportRequired, minAcceptanceQuorum, voteDuration] to set up the voting app of the organization + * @param _abcSettings Array of [uint256 entryTribute, uint256 exitTribute, address collateralToken, uint32 reserveRatio, uint256 initialBalance] + * for the Augmented Bonding Curve app. + */ + function newInstance( + string memory _id, + address[] memory _holders, + uint256[] memory _stakes, + uint64[3] memory _votingSettings, + uint256[5] memory _abcSettings + ) public { + _validateId(_id); + _ensureAbcSettings(_holders, _stakes, _votingSettings, _abcSettings); + + (Kernel dao, ACL acl) = _createDAO(); + (TokenManager tokenManager, Voting voting,, Agent agent) = + _unwrapApps(_setupApps(dao, acl, _holders, _stakes, _votingSettings)); + IAugmentedBondingCurve abc = _setupAbcApps(dao, acl, tokenManager, agent, _abcSettings); + _setupAbcPermissions(acl, abc, tokenManager, voting); + _transferRootPermissionsFromTemplateAndFinalizeDAO(dao, voting); + _registerID(_id, dao); + } + + function _setupApps( + Kernel _dao, + ACL _acl, + address[] memory _holders, + uint256[] memory _stakes, + uint64[3] memory _votingSettings + ) internal returns (address[4] memory) { + MiniMeToken token = _popTokenCache(msg.sender); + Agent agent = _installDefaultAgentApp(_dao); + Finance finance = _installFinanceApp(_dao, agent, DEFAULT_FINANCE_PERIOD); + TokenManager tokenManager = _installTokenManagerApp(_dao, token, TOKEN_TRANSFERABLE, TOKEN_MAX_PER_ACCOUNT); + Voting voting = _installVotingApp(_dao, token, _votingSettings); + + _mintTokens(_acl, tokenManager, _holders, _stakes); + _setupPermissions(_acl, agent, voting, finance, tokenManager); + + return [address(tokenManager), address(voting), address(finance), address(agent)]; + } + + function _setupAbcApps( + Kernel _dao, + ACL _acl, + TokenManager _tokenManager, + Agent _agent, + uint256[5] memory _abcSettings + ) internal returns (IAugmentedBondingCurve) { + ( + uint256 entryTribute, + uint256 exitTribute, + address collateralToken, + uint32 reserveRatio, + uint256 initialBalance + ) = _unwrapAbcSettings(_abcSettings); + + Vault reserve = + Vault(_installNonDefaultApp(_dao, VAULT_APP_ID, abi.encodeWithSelector(Vault(0).initialize.selector))); + IAugmentedBondingCurve abc = _installAbcApp(_dao, _tokenManager, reserve, _agent, entryTribute, exitTribute); + _configureAbcApp(_acl, abc, collateralToken, reserveRatio); + if (initialBalance > 0) { + require( + ERC20(collateralToken).safeTransferFrom(msg.sender, address(reserve), initialBalance), + ERROR_FUNDS_NOT_TRANSFERRED + ); + } + return abc; + } + + function _setupPermissions(ACL _acl, Agent _agent, Voting _voting, Finance _finance, TokenManager _tokenManager) + internal + { + _createAgentPermissions(_acl, _agent, _voting, _voting); + _createVaultPermissions(_acl, _agent, _finance, _voting); + _createFinancePermissions(_acl, _finance, _voting, _voting); + _createFinanceCreatePaymentsPermission(_acl, _finance, _voting, _voting); + _createEvmScriptsRegistryPermissions(_acl, _voting, _voting); + _createVotingPermissions(_acl, _voting, _voting, _tokenManager, _voting); + _createTokenManagerPermissions(_acl, _tokenManager, _voting, address(this)); + } + + function _setupAbcPermissions(ACL _acl, IAugmentedBondingCurve _abc, TokenManager _tokenManager, Voting _voting) + internal + { + _createAbcAndReservePermissions(_acl, _abc, _voting, _voting); + _acl.grantPermission(_abc, _tokenManager, _tokenManager.MINT_ROLE()); + _acl.grantPermission(_abc, _tokenManager, _tokenManager.BURN_ROLE()); + _acl.setPermissionManager(_voting, _tokenManager, _tokenManager.MINT_ROLE()); + _acl.setPermissionManager(_voting, _tokenManager, _tokenManager.BURN_ROLE()); + } + + function _ensureAbcSettings( + address[] memory _holders, + uint256[] memory _stakes, + uint64[3] memory _votingSettings, + uint256[5] memory _abcSettings + ) private pure { + require(_holders.length > 0, ERROR_EMPTY_HOLDERS); + require(_holders.length == _stakes.length, ERROR_BAD_HOLDERS_STAKES_LEN); + require(_votingSettings.length == 3, ERROR_BAD_VOTE_SETTINGS); + require(_abcSettings.length == 5, ERROR_BAD_ABC_SETTINGS); + } + + function _unwrapApps(address[4] memory _apps) private pure returns (TokenManager, Voting, Finance, Agent) { + return (TokenManager(_apps[0]), Voting(_apps[1]), Finance(_apps[2]), Agent(_apps[3])); + } +} diff --git a/pkg/abc-template/contracts/interfaces/IAugmentedBondingCurve.sol b/pkg/abc-template/contracts/interfaces/IAugmentedBondingCurve.sol new file mode 100644 index 0000000..c980b70 --- /dev/null +++ b/pkg/abc-template/contracts/interfaces/IAugmentedBondingCurve.sol @@ -0,0 +1,25 @@ +pragma solidity ^0.4.24; + +import "@aragon/apps-token-manager/contracts/TokenManager.sol"; +import "@aragon/apps-vault/contracts/Vault.sol"; + +interface IAugmentedBondingCurve { + function initialize( + TokenManager _tokenManager, + address _formula, + Vault _reserve, + address _beneficiary, + uint256 _entryTribute, + uint256 _exitTribute + ) external; + function MANAGE_COLLATERAL_TOKEN_ROLE() external returns (bytes32); + function MAKE_BUY_ORDER_ROLE() external returns (bytes32); + function MAKE_SELL_ORDER_ROLE() external returns (bytes32); + function addCollateralToken( + address _collateral, + uint256 _virtualSupply, + uint256 _virtualBalance, + uint32 _reserveRatio + ) external; + function reserve() external view returns (Vault); +} diff --git a/pkg/abc-template/hardhat.config.js b/pkg/abc-template/hardhat.config.js new file mode 100644 index 0000000..0c47f3b --- /dev/null +++ b/pkg/abc-template/hardhat.config.js @@ -0,0 +1,32 @@ +require("@nomicfoundation/hardhat-ethers"); +require("@nomicfoundation/hardhat-verify"); +require("@nomicfoundation/hardhat-toolbox"); + +/** @type import('hardhat/config').HardhatUserConfig */ +module.exports = { + solidity: { + version: "0.4.24", + settings: { + optimizer: { + enabled: true, + runs: 20000 + } + } + }, + networks: { + hardhat: { + forking: { + url: "https://opt-mainnet.g.alchemy.com/v2/" + } + }, + gnosis: { + url: "https://rpc.gnosischain.com/", + }, + optimism: { + url: "https://opt-mainnet.g.alchemy.com/v2/" + } + }, + etherscan: { + apiKey: "" + } +}; diff --git a/pkg/abc-template/package.json b/pkg/abc-template/package.json new file mode 100644 index 0000000..d7bd3fe --- /dev/null +++ b/pkg/abc-template/package.json @@ -0,0 +1,22 @@ +{ + "name": "hardhat-project", + "dependencies": { + "@aragon/os": "^4.4.0", + "@aragon/templates-shared": "^1.0.1", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", + "@nomicfoundation/hardhat-ethers": "^3.0.4", + "@nomicfoundation/hardhat-network-helpers": "^1.0.0", + "@nomicfoundation/hardhat-toolbox": "^3.0.0", + "@nomicfoundation/hardhat-verify": "^1.1.1", + "@typechain/ethers-v6": "^0.4.0", + "@typechain/hardhat": "^8.0.0", + "@types/mocha": ">=9.1.0", + "chai": "^4.3.10", + "ethers": "^6.4.0", + "hardhat": "^2.18.0", + "hardhat-gas-reporter": "^1.0.8", + "solidity-coverage": "^0.8.1", + "ts-node": ">=8.0.0", + "typechain": "^8.2.0" + } +} diff --git a/pkg/abc-template/scripts/deploy.js b/pkg/abc-template/scripts/deploy.js new file mode 100644 index 0000000..f5b61a5 --- /dev/null +++ b/pkg/abc-template/scripts/deploy.js @@ -0,0 +1,36 @@ +const { ethers } = require("hardhat"); + +async function main() { + + // Gnosis Chain + // const daoFactory = "0x4037F97fcc94287257E50Bd14C7DA9Cb4Df18250"; + // const ens = "0xaAfCa6b0C89521752E559650206D7c925fD0e530"; + // const miniMeFactory = "0xf7d36d4d46CDA364eDc85E5561450183469484C5"; + // const aragonID = "0x0B3b17F9705783Bb51Ae8272F3245D6414229B36"; + // const formula = "0xA4e28453b4F3fcB251EEbe1aC2798eEE55e2bE6a"; + + + + // Optimism: + const daoFactory = "0x0a42106615233D0E6F9811d0cBb7ddC83170Fe5E" + const ens = "0x6f2CA655f58d5fb94A08460aC19A552EB19909FD" + const miniMeFactory = "0xb5314953f96e12cab6BdB9eaa79922e657783142" + const aragonID = "0x44ADB013bE98F04d9E525d033E2D85Ce5E195D8F" + const formula = "0x29c7e9fff64bde42faa6e8c0567a15290f7d948b" + + const template = await ethers.deployContract("AbcTemplate", [daoFactory, ens, miniMeFactory, aragonID, formula]); + + await template.waitForDeployment(); + + console.log( + `Contract deployed at ${template.target}` + ); + +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/pkg/abc-template/test/AbcTemplate.test.ts b/pkg/abc-template/test/AbcTemplate.test.ts new file mode 100644 index 0000000..3ddf0fa --- /dev/null +++ b/pkg/abc-template/test/AbcTemplate.test.ts @@ -0,0 +1,49 @@ +const { ethers, network } = require("hardhat"); + +// Optimism: +const daoFactory = "0x0a42106615233D0E6F9811d0cBb7ddC83170Fe5E" +const _ens = "0x6f2CA655f58d5fb94A08460aC19A552EB19909FD" +const _miniMeFactory = "0xb5314953f96e12cab6BdB9eaa79922e657783142" +const _aragonID = "0x44ADB013bE98F04d9E525d033E2D85Ce5E195D8F" +const _formula = "0x29c7e9fff64bde42faa6e8c0567a15290f7d948b" +const _reserveToken = "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1" + +const pct = (n) => BigInt(n) * BigInt(10 ** 16); +const wad = (n) => BigInt(n) * BigInt(10 ** 18); + +describe("AbcTemplate contract", function () { + it("Deployment should work", async function () { + const [owner] = await ethers.getSigners(); + + const daiHolder = '0x2de373887b9742162c9a5885ddb5debea8e4486d' + await network.provider.request({ + method: "hardhat_impersonateAccount", + params: [daiHolder], + }); + const signer = await ethers.getSigner(daiHolder); + + const abcTemplate = await ethers.deployContract("AbcTemplate", [ + daoFactory, + _ens, + _miniMeFactory, + _aragonID, + _formula + ]); + + const reserveToken = new ethers.Contract(_reserveToken, ["function approve(address spender, uint256 value) public returns (bool)"], signer); + + await reserveToken.approve(await abcTemplate.getAddress(), wad(3n * 10n ** 18n)); + + await abcTemplate.connect(signer).newTokenAndInstance( + "Token Name", + "TKN", + "daoname", + [owner.address], + [wad(1)], + [pct(50), pct(15), 7 * 24 * 60 * 60], + [pct(1), pct(2), _reserveToken, 20e4, wad(3n * 10n ** 18n)], + { from : daiHolder } + ); + + }); +});