Skip to content

Commit

Permalink
♻() Update after review
Browse files Browse the repository at this point in the history
  • Loading branch information
pgonday committed Dec 17, 2024
1 parent 781a5a1 commit 7bf4a98
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 28 deletions.
108 changes: 85 additions & 23 deletions contracts/compliance/modular/modules/InitialLockupPeriodModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,23 @@

pragma solidity 0.8.27;

import "../IModularCompliance.sol";
import "../../../token/IToken.sol";
import "./AbstractModuleUpgradeable.sol";

/// @dev Error emitted when the user has insufficient balance because of locked tokens.
/// @param user the address of the user.
/// @param value the value of the transfer.
/// @param availableAmount the available amount of unlocked tokens.
error InsufficientBalanceTokensLocked(address user, uint256 value, uint256 availableAmount);

event LockupPeriodSet(address indexed compliance, uint256 lockupPeriod);

/// @notice Event emitted when the lockup period is set.
/// @param compliance the address of the compliance contract.
/// @param lockupPeriodInDays the lockup period in days.
event LockupPeriodSet(address indexed compliance, uint256 lockupPeriodInDays);

/// @title InitialLockupPeriodModule
/// @notice Enforces a lockup period for all investors whenever they receive tokens through primary emissions
contract InitialLockupPeriodModule is AbstractModuleUpgradeable {

struct LockedTokens {
Expand All @@ -83,29 +93,32 @@ contract InitialLockupPeriodModule is AbstractModuleUpgradeable {
}

mapping(address compliance => uint256 lockupPeriod) private _lockupPeriods;
mapping(address compliance => mapping(address user => LockedTokens[] lockedTokens)) private _lockedTokens;
mapping(address compliance => mapping(address user => LockedTokens[])) private _lockedTokens;

/// @dev initializes the contract and sets the initial state.
function initialize() external initializer {
__AbstractModule_init();
}

/// @dev sets the lockup period for a compliance contract.
/// @param _lockupPeriod the lockup period in seconds.
function setLockupPeriod(uint256 _lockupPeriod) external onlyComplianceCall {
_lockupPeriods[msg.sender] = _lockupPeriod;
/// @param _lockupPeriodInDays the lockup period in days.
function setLockupPeriod(uint256 _lockupPeriodInDays) external onlyComplianceCall {
_lockupPeriods[msg.sender] = _lockupPeriodInDays * 1 days;

emit LockupPeriodSet(msg.sender, _lockupPeriod);
emit LockupPeriodSet(msg.sender, _lockupPeriodInDays);
}

/// @inheritdoc IModule
function moduleTransferAction(address _from, address /*_to*/, uint256 _value) external override onlyComplianceCall {
if (_from != address(0)) {
// Check if the user has enough unlocked tokens to transfer
uint256 availableAmount = _calculateAvailableAmount(msg.sender, _from);
if (_value > availableAmount) {
revert InsufficientBalanceTokensLocked(_from, _value, availableAmount);
}
function moduleTransferAction(address _from, address /*_to*/, uint256 _value) external override onlyComplianceCall {
if (_from == address(0)) {
return;
}

(uint256 lockedAmount, uint256 unlockedAmount) = _calculateLockedAmount(msg.sender, _from);
uint256 freeAmount =
IToken(IModularCompliance(msg.sender).getTokenBound()).balanceOf(_from) - lockedAmount - unlockedAmount;
if (_value > freeAmount) {
_updateLockedTokens(_from, _value - freeAmount);
}
}

Expand All @@ -120,17 +133,32 @@ contract InitialLockupPeriodModule is AbstractModuleUpgradeable {
}

/// @inheritdoc IModule
// solhint-disable-next-line no-empty-blocks
function moduleBurnAction(address _from, uint256 _value) external override onlyComplianceCall { }
function moduleBurnAction(address _from, uint256 _value) external override onlyComplianceCall {
(uint256 lockedAmount, uint256 unlockedAmount) = _calculateLockedAmount(msg.sender, _from);
uint256 previousBalance = IToken(IModularCompliance(msg.sender).getTokenBound()).balanceOf(_from) + _value;

require(
(previousBalance - lockedAmount) >= _value,
InsufficientBalanceTokensLocked(_from, _value, previousBalance - lockedAmount)
);

uint256 freeAmount = previousBalance - lockedAmount - unlockedAmount;
if (_value > freeAmount) {
_updateLockedTokens(_from, _value - freeAmount);
}
}

/// @inheritdoc IModule
function moduleCheck(address _from, address /*_to*/, uint256 _value, address _compliance) external
view override returns (bool) {
return _from == address(0) || _calculateAvailableAmount(_compliance, _from) >= _value;
(uint256 lockedAmount, ) = _calculateLockedAmount(_compliance, _from);

return _from == address(0)
|| IToken(IModularCompliance(_compliance).getTokenBound()).balanceOf(_from) - lockedAmount >= _value;
}

/// @inheritdoc IModule
function canComplianceBind(address /*_compliance*/) external view override returns (bool) {
function canComplianceBind(address /*_compliance*/) external pure override returns (bool) {
return true;
}

Expand All @@ -144,15 +172,49 @@ contract InitialLockupPeriodModule is AbstractModuleUpgradeable {
return "InitialLockupPeriodModule";
}

/// @dev calculates the available amount of unlocked tokens for a user.
/// @dev updates the locked tokens for a user.
/// @param _user the address of the user.
/// @param _value the amount of tokens to unlock.
function _updateLockedTokens(address _user, uint256 _value) internal {
LockedTokens[] storage lockedTokens = _lockedTokens[msg.sender][_user];
for (uint256 i; _value > 0 && i < lockedTokens.length; ) {
if (lockedTokens[i].releaseTimestamp <= block.timestamp) {
if (_value >= lockedTokens[i].amount) {
_value -= lockedTokens[i].amount;

// Remove entry
if (i == lockedTokens.length - 1) {
lockedTokens.pop();
break;
} else {
lockedTokens[i] = lockedTokens[lockedTokens.length - 1];
lockedTokens.pop();
}
} else {
lockedTokens[i].amount -= _value;
break;
}
}
else {
i++;
}
}
}

/// @dev calculates the locked amount of tokens for a user.
/// @param _compliance the address of the compliance contract.
/// @param _user the address of the user.
/// @return _availableAmount the available amount of unlocked tokens.
function _calculateAvailableAmount(address _compliance, address _user) internal view returns (uint256 _availableAmount) {
/// @return _lockedAmount the locked amount of tokens.
/// @return _unlockedAmount the unlocked amount of tokens.
function _calculateLockedAmount(address _compliance, address _user) internal view
returns (uint256 _lockedAmount, uint256 _unlockedAmount) {
uint256 periodsLength = _lockedTokens[_compliance][_user].length;
for (uint256 i; i < periodsLength; i++) {
if (_lockedTokens[_compliance][_user][i].releaseTimestamp <= block.timestamp) {
_availableAmount += _lockedTokens[_compliance][_user][i].amount;
if (_lockedTokens[_compliance][_user][i].releaseTimestamp > block.timestamp) {
_lockedAmount += _lockedTokens[_compliance][_user][i].amount;
}
else {
_unlockedAmount += _lockedTokens[_compliance][_user][i].amount;
}
}
}
Expand Down
33 changes: 28 additions & 5 deletions test/compliances/module-initial-lockup-period.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ describe('InitialLockupPeriodModule', () => {
};
}

async function increaseTimestamp(timestamp: number) {
await ethers.provider.send('evm_increaseTime', [timestamp]);
async function increaseTimestamp(days: number) {
await ethers.provider.send('evm_increaseTime', [days * 24 * 60 * 60]);
await ethers.provider.send('evm_mine', []);
}

Expand Down Expand Up @@ -68,7 +68,7 @@ describe('InitialLockupPeriodModule', () => {
accounts: { aliceWallet },
} = await loadFixture(deployInitialLockupPeriodModuleFullSuite);

await expect(complianceModule.connect(aliceWallet).setLockupPeriod(100)).to.be.revertedWithCustomError(
await expect(complianceModule.connect(aliceWallet).setLockupPeriod(10)).to.be.revertedWithCustomError(
complianceModule,
'OnlyBoundComplianceCanCall',
);
Expand All @@ -79,7 +79,7 @@ describe('InitialLockupPeriodModule', () => {
suite: { compliance, complianceModule },
} = await loadFixture(deployInitialLockupPeriodModuleFullSuite);

const lockupPeriod = 100;
const lockupPeriod = 10;
const tx = await setLockupPeriod(compliance, complianceModule, lockupPeriod);

await expect(tx).to.emit(complianceModule, 'LockupPeriodSet').withArgs(compliance.target, lockupPeriod);
Expand All @@ -93,7 +93,7 @@ describe('InitialLockupPeriodModule', () => {
accounts: { aliceWallet, bobWallet, tokenAgent, complianceSigner },
} = await loadFixture(deployInitialLockupPeriodModuleFullSuite);

const lockupPeriod = 100;
const lockupPeriod = 10;
await setLockupPeriod(compliance, complianceModule, lockupPeriod);

await token.connect(tokenAgent).mint(aliceWallet.address, 100);
Expand Down Expand Up @@ -143,4 +143,27 @@ describe('InitialLockupPeriodModule', () => {
});
});
});

describe('Burn Checks', () => {
it('should allow burn after lockup period', async () => {
const {
suite: { compliance, complianceModule, token },
accounts: { aliceWallet, tokenAgent },
} = await loadFixture(deployInitialLockupPeriodModuleFullSuite);

const lockupPeriod = 10;
await setLockupPeriod(compliance, complianceModule, lockupPeriod);

await token.connect(tokenAgent).mint(aliceWallet.address, 100);

// Burn will fail because the lockup period is not over
await expect(token.connect(tokenAgent).burn(aliceWallet.address, 100))
.to.be.revertedWithCustomError(complianceModule, 'InsufficientBalanceTokensLocked')
.withArgs(aliceWallet.address, 100, 0);

// Burn will succeed because the lockup period is over
await increaseTimestamp(lockupPeriod);
await token.connect(tokenAgent).burn(aliceWallet.address, 100);
});
});
});

0 comments on commit 7bf4a98

Please sign in to comment.