Skip to content

Commit

Permalink
Merge pull request #319 from corpus-io/feature/createLockupContracts
Browse files Browse the repository at this point in the history
move vesting lockup creation to VestingCloneFactory
  • Loading branch information
malteish authored Jan 5, 2024
2 parents 800e71a + 95f349a commit e5483d9
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 25 deletions.
38 changes: 14 additions & 24 deletions contracts/factories/PrivateOfferFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -64,34 +64,24 @@ contract PrivateOfferFactory {

// deploy the vesting contract
Vesting vesting = Vesting(
vestingCloneFactory.createVestingClone(salt, trustedForwarder, address(this), address(_arguments.token))
vestingCloneFactory.createVestingCloneWithLockupPlan(
salt,
trustedForwarder,
_vestingContractOwner,
address(_arguments.token),
_arguments.tokenAmount,
_arguments.tokenReceiver,
_vestingStart,
_vestingCliff,
_vestingDuration
)
);

// create the vesting plan
vesting.createVesting(
_arguments.tokenAmount,
_arguments.tokenReceiver,
_vestingStart,
_vestingCliff,
_vestingDuration,
false
); // this plan is not mintable

vesting.removeManager(address(this));

// transfer ownership of the vesting contract
if (_vestingContractOwner == address(0)) {
// if the owner is 0, the vesting contract will not have an owner. So no one can interfere with the vesting.
vesting.renounceOwnership();
} else {
vesting.transferOwnership(_vestingContractOwner);
}

// deploy the private offer
// update currency receiver to be the vesting contract
PrivateOfferArguments memory calldataArguments = _arguments;
calldataArguments.tokenReceiver = address(vesting);
// update currency receiver to be the vesting contract

// deploy the private offer
address privateOffer = _deployPrivateOffer(_rawSalt, calldataArguments);

require(_arguments.token.balanceOf(address(vesting)) == _arguments.tokenAmount, "Execution failed");
Expand Down Expand Up @@ -131,7 +121,7 @@ contract PrivateOfferFactory {
address vestingAddress = vestingCloneFactory.predictCloneAddress(
salt,
trustedForwarder,
address(this),
address(vestingCloneFactory),
address(_arguments.token)
);

Expand Down
47 changes: 46 additions & 1 deletion contracts/factories/VestingCloneFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ contract VestingCloneFactory is CloneFactory {
address _trustedForwarder,
address _owner,
address _token
) external returns (address) {
) public returns (address) {
bytes32 salt = keccak256(abi.encode(_rawSalt, _trustedForwarder, _owner, _token));
address clone = Clones.cloneDeterministic(implementation, salt);
Vesting vesting = Vesting(clone);
Expand All @@ -36,6 +36,51 @@ contract VestingCloneFactory is CloneFactory {
return clone;
}

/**
* Create a new vesting clone with a lockup plan. The contract ownership can be renounced in the same transaction,
* leaving the contract without an owner and thus without any way to change the vesting plan or add other plans.
* @dev This function creates a transferrable vesting plan.
* @param _rawSalt value that influences the address of the clone, but not the initialization
* @param _trustedForwarder the trusted forwarder (ERC2771) can not be changed, but is checked for security
* @param _owner future owner of the vesting contract. If 0, the contract will not have an owner.
* @param _token token to vest
* @param _allocation amount of tokens to vest
* @param _beneficiary address receiving the tokens
* @param _start start date of the vesting
* @param _cliff cliff duration
* @param _duration total duration
*/
function createVestingCloneWithLockupPlan(
bytes32 _rawSalt,
address _trustedForwarder,
address _owner,
address _token,
uint256 _allocation,
address _beneficiary,
uint64 _start,
uint64 _cliff,
uint64 _duration
) external returns (address) {
// deploy the vesting contract
Vesting vesting = Vesting(createVestingClone(_rawSalt, _trustedForwarder, address(this), _token));

// create the vesting plan
vesting.createVesting(_allocation, _beneficiary, _start, _cliff, _duration, false); // this plan is not mintable

// remove the manager role from the vesting contract
vesting.removeManager(address(this));

// transfer ownership of the vesting contract
if (_owner == address(0)) {
// if the owner is 0, the vesting contract will not have an owner. So no one can interfere with the vesting.
vesting.renounceOwnership();
} else {
vesting.transferOwnership(_owner);
}

return address(vesting);
}

/**
* Calculate the address a clone will have using the given parameters
* @param _rawSalt value that influences the address of the clone, but not the initialization
Expand Down
158 changes: 158 additions & 0 deletions test/PrivateOfferOffchainPaymentTimeLock.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity 0.8.23;

import "../lib/forge-std/src/Test.sol";
import "../contracts/factories/TokenProxyFactory.sol";
import "../contracts/factories/VestingCloneFactory.sol";
import "./resources/CloneCreators.sol";
import "./resources/FakePaymentToken.sol";

contract PrivateOfferOffchainPaymentTimeLockTest is Test {
AllowList list;
FeeSettings feeSettings;
Token token;
FakePaymentToken currency;
VestingCloneFactory vestingCloneFactory;

uint256 MAX_INT = type(uint256).max;

address public constant admin = 0x0109709eCFa91a80626FF3989D68f67f5b1dD120;
address public constant tokenReceiver = 0x1109709ecFA91a80626ff3989D68f67F5B1Dd121;
address public constant mintAllower = 0x2109709EcFa91a80626Ff3989d68F67F5B1Dd122;
address public constant currencyPayer = 0x3109709ECfA91A80626fF3989D68f67F5B1Dd123;
address public constant owner = 0x6109709EcFA91A80626FF3989d68f67F5b1dd126;
address public constant currencyReceiver = 0x7109709eCfa91A80626Ff3989D68f67f5b1dD127;
address public constant paymentTokenProvider = 0x8109709ecfa91a80626fF3989d68f67F5B1dD128;
address public constant trustedForwarder = 0x9109709EcFA91A80626FF3989D68f67F5B1dD129;

uint256 public constant price = 10000000;

uint256 requirements = 92785934;

function setUp() public {
Vesting vestingImplementation = new Vesting(trustedForwarder);
vestingCloneFactory = new VestingCloneFactory(address(vestingImplementation));

vm.prank(paymentTokenProvider);
currency = new FakePaymentToken(0, 18);

list = createAllowList(trustedForwarder, address(this));
list.set(tokenReceiver, requirements);
list.set(address(currency), TRUSTED_CURRENCY);

Fees memory fees = Fees(100, 100, 100, 0);
feeSettings = createFeeSettings(trustedForwarder, address(this), fees, admin, admin, admin);

Token implementation = new Token(trustedForwarder);
TokenProxyFactory tokenCloneFactory = new TokenProxyFactory(address(implementation));
token = Token(
tokenCloneFactory.createTokenProxy(
0,
trustedForwarder,
feeSettings,
admin,
list,
requirements,
"token",
"TOK"
)
);
}

/**
*
* @param salt can be used to generate different addresses
* @param releaseStartTime when to start releasing tokens
* @param attemptTime try to release tokens after this amount of time
* @param releaseDuration how long the releasing of tokens should take
*/
function testPrivateOfferWithTimeLock(
bytes32 salt,
uint64 releaseStartTime,
uint64 releaseDuration,
uint64 attemptTime
) public {
vm.assume(releaseStartTime > attemptTime);
vm.assume(releaseDuration < 20 * 365 * 24 * 60 * 60); // 20 years
vm.assume(type(uint64).max - releaseDuration - 1 - block.timestamp > releaseStartTime);
vm.assume(attemptTime < releaseStartTime + releaseDuration);
vm.assume(attemptTime > 1);
vm.assume(releaseStartTime > 1);

// reference all times to current time. Important for when testing with mainnet forks.
uint64 testStartTime = uint64(block.timestamp);
attemptTime += testStartTime;
releaseStartTime += testStartTime;
assertTrue(testStartTime < releaseStartTime, "testStartTime >= releaseStartTime");

uint256 tokenAmount = 20000000000000;

// as the payment happens off-chain, we just assume it happened

// predict addresses
address expectedTimeLockAddress = vestingCloneFactory.predictCloneAddress(
salt,
trustedForwarder,
address(vestingCloneFactory),
address(token)
);

// add time lock and token receiver to the allow list
list.set(expectedTimeLockAddress, requirements);
list.set(tokenReceiver, requirements);

// make sure balances are as expected before deployment
assertEq(token.balanceOf(expectedTimeLockAddress), 0, "timeLock wrong token balance before deployment");

// create vesting contract and mint tokens
Vesting timeLock = Vesting(
vestingCloneFactory.createVestingCloneWithLockupPlan(
salt,
trustedForwarder,
address(0), // no owner
address(token),
tokenAmount,
tokenReceiver,
releaseStartTime,
releaseDuration,
releaseDuration
)
);
vm.prank(admin);
token.mint(address(timeLock), tokenAmount);

// check vesting contract
console.log("timeLock token balance: %s", token.balanceOf(address(timeLock)));

assertEq(token.balanceOf(address(timeLock)), tokenAmount, "timeLock wrong token balance after deployment");

assertEq(
token.balanceOf(token.feeSettings().privateOfferFeeCollector(address(token))),
token.feeSettings().tokenFee(tokenAmount, address(token)),
"feeCollector token balance is not correct"
);

/*
* PrivateOffer worked properly, now test the time lock
*/
// immediate release should not work
assertEq(token.balanceOf(tokenReceiver), 0, "investor vault should have no tokens");
vm.prank(tokenReceiver);
timeLock.release(uint64(1));
assertEq(token.balanceOf(tokenReceiver), 0, "investor vault should still have no tokens");

// too early release should not work
vm.warp(attemptTime);
vm.prank(tokenReceiver);
timeLock.release(uint64(1));
assertEq(token.balanceOf(tokenReceiver), 0, "investor vault should still be empty");

// not testing the linear release time here because it's already tested in the vesting wallet tests

// release all tokens after release duration has passed
vm.warp(releaseStartTime + releaseDuration + 1);
vm.prank(tokenReceiver);
timeLock.release(uint64(1));
assertEq(token.balanceOf(tokenReceiver), tokenAmount, "investor vault should have all tokens");
}
}

0 comments on commit e5483d9

Please sign in to comment.