Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: New Adapter - KYC Onboarding #437

Merged
merged 16 commits into from
Nov 24, 2021
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
371 changes: 371 additions & 0 deletions contracts/adapters/KycOnboarding.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,371 @@
pragma solidity ^0.8.0;

// SPDX-License-Identifier: MIT

import "../core/DaoRegistry.sol";
import "../extensions/bank/Bank.sol";
import "../guards/AdapterGuard.sol";
import "../utils/Signatures.sol";
import "../helpers/WETH.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/**
MIT License

Copyright (c) 2021 Openlaw

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

contract KycOnboardingContract is AdapterGuard, Signatures {
using Address for address payable;
using SafeERC20 for IERC20;

event Onboarded(DaoRegistry dao, address member, uint256 units);
struct Coupon {
address kycedMember;
}

struct OnboardingDetails {
uint88 chunkSize;
uint88 numberOfChunks;
uint88 unitsPerChunk;
uint88 unitsRequested;
uint88 maximumTotalUnits;
uint160 amount;
}

string public constant COUPON_MESSAGE_TYPE = "Message(address kycedMember)";
bytes32 public constant COUPON_MESSAGE_TYPEHASH =
keccak256(abi.encodePacked(COUPON_MESSAGE_TYPE));

bytes32 constant SignerAddressConfig =
keccak256("kyc-onboarding.signerAddress");

bytes32 constant ChunkSize = keccak256("kyc-onboarding.chunkSize");
bytes32 constant UnitsPerChunk = keccak256("kyc-onboarding.unitsPerChunk");
bytes32 constant MaximumChunks = keccak256("kyc-onboarding.maximumChunks");
bytes32 constant MaximumUnits =
keccak256("kyc-onboarding.maximumTotalUnits");
bytes32 constant MaxMembers = keccak256("kyc-onboarding.maxMembers");
bytes32 constant FundTargetAddress =
keccak256("kyc-onboarding.fundTargetAddress");
bytes32 constant TokensToMint = keccak256("kyc-onboarding.tokensToMint");

WETH private _weth;
IERC20 private _weth20;

mapping(DaoRegistry => mapping(address => uint256)) public totalUnits;

constructor(address payable weth) {
_weth = WETH(weth);
_weth20 = IERC20(weth);
}

/**
* @notice Configures the Adapter with the coupon signer address and token to mint.
* @param dao the dao to configure
* @param signerAddress is the DAO instance to be configured
* @param chunkSize how many wei per chunk
* @param unitsPerChunk how many units do we get per chunk
* @param maximumChunks maximum number of chunks allowed
* @param maxUnits how many internal tokens can be minted
* @param maxMembers maximum number of members allowed to join
* @param fundTargetAddress multisig address to transfer the money from, set it to address(0) if you dont want to use a multisig
* @param tokenAddr the token in which the onboarding can take place
* @param internalTokensToMint the token that will be minted when the member joins the DAO
*/
function configureDao(
DaoRegistry dao,
address signerAddress,
uint256 chunkSize,
uint256 unitsPerChunk,
uint256 maximumChunks,
uint256 maxUnits,
uint256 maxMembers,
address fundTargetAddress,
address tokenAddr,
address internalTokensToMint
) external onlyAdapter(dao) {
require(
chunkSize > 0 && chunkSize < type(uint88).max,
"chunkSize::invalid"
);
require(
maxMembers > 0 && maxMembers < type(uint88).max,
"maxMembers::invalid"
);
require(
maximumChunks > 0 && maximumChunks < type(uint88).max,
"maximumChunks::invalid"
);
require(
maxUnits > 0 && maxUnits < type(uint88).max,
"maxUnits::invalid"
);
require(
unitsPerChunk > 0 && unitsPerChunk < type(uint88).max,
"unitsPerChunk::invalid"
);
require(
maximumChunks * unitsPerChunk < type(uint88).max,
"potential overflow"
);

require(
DaoHelper.isNotZeroAddress(signerAddress),
"signer address is nil!"
);

require(
DaoHelper.isNotZeroAddress(internalTokensToMint),
"null internal token address"
);

dao.setAddressConfiguration(
_configKey(tokenAddr, SignerAddressConfig),
signerAddress
);
dao.setAddressConfiguration(
_configKey(tokenAddr, FundTargetAddress),
fundTargetAddress
);
dao.setConfiguration(_configKey(tokenAddr, ChunkSize), chunkSize);
dao.setConfiguration(
_configKey(tokenAddr, UnitsPerChunk),
unitsPerChunk
);
dao.setConfiguration(
_configKey(tokenAddr, MaximumChunks),
maximumChunks
);
dao.setConfiguration(_configKey(tokenAddr, MaximumUnits), maxUnits);
dao.setConfiguration(_configKey(tokenAddr, MaxMembers), maxMembers);
dao.setAddressConfiguration(
_configKey(tokenAddr, TokensToMint),
internalTokensToMint
);

BankExtension bank = BankExtension(
dao.getExtensionAddress(DaoHelper.BANK)
);
bank.registerPotentialNewInternalToken(DaoHelper.UNITS);
bank.registerPotentialNewToken(tokenAddr);
}

/**
* @notice Hashes the provided coupon as an ERC712 hash.
* @param dao is the DAO instance to be configured
* @param coupon is the coupon to hash
*/
function hashCouponMessage(DaoRegistry dao, Coupon memory coupon)
public
view
returns (bytes32)
{
bytes32 message = keccak256(
abi.encode(COUPON_MESSAGE_TYPEHASH, coupon.kycedMember)
);

return hashMessage(dao, block.chainid, address(this), message);
}

/**
* @notice Starts the onboarding propocess of a kyc member that is using ETH to join the DAO.
* @param kycedMember The address of the kyced member that wants to join the DAO.
* @param signature The signature that will be verified to redeem the coupon.
*/
function onboardEth(
DaoRegistry dao,
address kycedMember,
bytes memory signature
) external payable {
_onboard(dao, kycedMember, DaoHelper.ETH_TOKEN, msg.value, signature);
}

/**
* @notice Starts the onboarding propocess of a kyc member that is any ERC20 token to join the DAO.
* @param kycedMember The address of the kyced member that wants to join the DAO.
* @param tokenAddr The address of the ERC20 token that contains that funds of the kycedMember.
* @param amount The amount in ERC20 that will be contributed to the DAO in exchange for the DAO units.
* @param signature The signature that will be verified to redeem the coupon.
*/
function onboard(
DaoRegistry dao,
address kycedMember,
address tokenAddr,
uint256 amount,
bytes memory signature
) external {
_onboard(dao, kycedMember, tokenAddr, amount, signature);
}

/**
* @notice Redeems a coupon to add a new member
* @param dao is the DAO instance to be configured
* @param kycedMember is the address that this coupon authorized to become a new member
* @param tokenAddr is the address the ETH address(0) or an ERC20 Token address
* @param signature is message signature for verification
*/
function _onboard(
DaoRegistry dao,
address kycedMember,
address tokenAddr,
uint256 amount,
bytes memory signature
) internal reentrancyGuard(dao) {
require(!dao.isActiveMember(dao, kycedMember), "already member");
uint256 maxMembers = dao.getConfiguration(
_configKey(tokenAddr, MaxMembers)
);
require(maxMembers > 0, "token not configured");
require(dao.getNbMembers() < maxMembers, "the DAO is full");

_checkKycCoupon(dao, kycedMember, tokenAddr, signature);
OnboardingDetails memory details = _checkData(dao, tokenAddr, amount);

BankExtension bank = BankExtension(
dao.getExtensionAddress(DaoHelper.BANK)
);
DaoHelper.potentialNewMember(kycedMember, dao, bank);
totalUnits[dao][tokenAddr] += details.unitsRequested;
address payable multisigAddress = payable(
dao.getAddressConfiguration(
_configKey(tokenAddr, FundTargetAddress)
)
);
if (multisigAddress == address(0x0)) {
adridadou marked this conversation as resolved.
Show resolved Hide resolved
if (tokenAddr == DaoHelper.ETH_TOKEN) {
bank.addToBalance{value: details.amount}(
DaoHelper.GUILD,
DaoHelper.ETH_TOKEN,
details.amount
);
} else {
bank.addToBalance(DaoHelper.GUILD, tokenAddr, details.amount);
IERC20 erc20 = IERC20(tokenAddr);
erc20.safeTransferFrom(
msg.sender,
address(bank),
details.amount
);
}
} else {
if (tokenAddr == DaoHelper.ETH_TOKEN) {
_weth.deposit{value: details.amount}();
_weth20.safeTransferFrom(
address(this),
multisigAddress,
details.amount
);
} else {
IERC20 erc20 = IERC20(tokenAddr);
erc20.safeTransferFrom(
msg.sender,
multisigAddress,
details.amount
);
}
}

bank.addToBalance(kycedMember, DaoHelper.UNITS, details.unitsRequested);

if (amount > details.amount && tokenAddr == DaoHelper.ETH_TOKEN) {
payable(msg.sender).sendValue(msg.value - details.amount);
}

emit Onboarded(dao, kycedMember, details.unitsRequested);
}

/**
* @notice Verifies if the given amount is enough to join the DAO
*/
function _checkData(
DaoRegistry dao,
address tokenAddr,
uint256 amount
) internal view returns (OnboardingDetails memory details) {
details.chunkSize = uint88(
dao.getConfiguration(_configKey(tokenAddr, ChunkSize))
);
require(details.chunkSize > 0, "config chunkSize missing");
details.numberOfChunks = uint88(amount / details.chunkSize);
require(details.numberOfChunks > 0, "insufficient funds");
require(
details.numberOfChunks <=
dao.getConfiguration(_configKey(tokenAddr, MaximumChunks)),
"too much funds"
);

details.unitsPerChunk = uint88(
dao.getConfiguration(_configKey(tokenAddr, UnitsPerChunk))
);

require(details.unitsPerChunk > 0, "config unitsPerChunk missing");
details.amount = details.numberOfChunks * details.chunkSize;
details.unitsRequested = details.numberOfChunks * details.unitsPerChunk;
details.maximumTotalUnits = uint88(
dao.getConfiguration(_configKey(tokenAddr, MaximumUnits))
);

require(
details.unitsRequested + totalUnits[dao][tokenAddr] <=
details.maximumTotalUnits,
"over max total units"
);
}

/**
* @notice Checks if the given signature is valid, if so the member is allowed to reedem the coupon and join the DAO.
* @param kycedMember is the address that this coupon authorized to become a new member
* @param tokenAddr is the address the ETH address(0) or an ERC20 Token address.
* @param signature is message signature for verification
*/
function _checkKycCoupon(
DaoRegistry dao,
address kycedMember,
address tokenAddr,
bytes memory signature
) internal view {
require(
ECDSA.recover(
hashCouponMessage(dao, Coupon(kycedMember)),
signature
) ==
dao.getAddressConfiguration(
_configKey(tokenAddr, SignerAddressConfig)
),
"invalid sig"
);
}

/**
* @notice Builds the configuration key by encoding an address with a string key.
* @param tokenAddrToMint The address to encode.
* @param key The key to encode.
*/
function _configKey(address tokenAddrToMint, bytes32 key)
internal
pure
returns (bytes32)
{
return keccak256(abi.encode(tokenAddrToMint, key));
}
}
2 changes: 1 addition & 1 deletion contracts/helpers/DaoHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ library DaoHelper {
keccak256("kick-bad-reporter-adpt");
bytes32 internal constant COUPON_ONBOARDING_ADPT =
keccak256("coupon-onboarding");
bytes32 internal constant LEND_NFT_ADPT = keccak256("coupon-onboarding");
bytes32 internal constant LEND_NFT_ADPT = keccak256("lend-nft");
bytes32 internal constant ERC20_TRANSFER_STRATEGY_ADPT =
keccak256("erc20-transfer-strategy");

Expand Down
Loading