3496126253
Add the self-contained Solidity 0.8.28 transfer-control prototype and supporting README for the Swift Hackathon 2026 submission.
361 lines
11 KiB
Solidity
361 lines
11 KiB
Solidity
pragma solidity ^0.8.28;
|
|
|
|
contract Owned {
|
|
address public owner;
|
|
|
|
modifier onlyOwner() {
|
|
require(msg.sender == owner, "ONLY_OWNER");
|
|
_;
|
|
}
|
|
|
|
constructor() {
|
|
owner = msg.sender;
|
|
}
|
|
|
|
function transferOwnership(address newOwner) external onlyOwner {
|
|
require(newOwner != address(0), "ZERO_OWNER");
|
|
owner = newOwner;
|
|
}
|
|
}
|
|
|
|
contract IdentityRegistry is Owned {
|
|
struct Party {
|
|
bytes32 partyId;
|
|
bytes32 accountId;
|
|
bytes32 jurisdiction;
|
|
bool verified;
|
|
bool eligible;
|
|
bool frozen;
|
|
}
|
|
|
|
mapping(address => Party) public parties;
|
|
|
|
event PartySet(
|
|
address indexed wallet,
|
|
bytes32 partyId,
|
|
bytes32 accountId,
|
|
bytes32 jurisdiction,
|
|
bool verified,
|
|
bool eligible,
|
|
bool frozen
|
|
);
|
|
|
|
function setParty(
|
|
address wallet,
|
|
bytes32 partyId,
|
|
bytes32 accountId,
|
|
bytes32 jurisdiction,
|
|
bool verified,
|
|
bool eligible,
|
|
bool frozen
|
|
) external onlyOwner {
|
|
parties[wallet] = Party({
|
|
partyId: partyId,
|
|
accountId: accountId,
|
|
jurisdiction: jurisdiction,
|
|
verified: verified,
|
|
eligible: eligible,
|
|
frozen: frozen
|
|
});
|
|
|
|
emit PartySet(wallet, partyId, accountId, jurisdiction, verified, eligible, frozen);
|
|
}
|
|
|
|
function setFrozen(address wallet, bool frozen) external onlyOwner {
|
|
parties[wallet].frozen = frozen;
|
|
emit PartySet(
|
|
wallet,
|
|
parties[wallet].partyId,
|
|
parties[wallet].accountId,
|
|
parties[wallet].jurisdiction,
|
|
parties[wallet].verified,
|
|
parties[wallet].eligible,
|
|
frozen
|
|
);
|
|
}
|
|
|
|
function getParty(address wallet) external view returns (Party memory) {
|
|
return parties[wallet];
|
|
}
|
|
}
|
|
|
|
contract RoleRegistry is Owned {
|
|
mapping(bytes32 => mapping(address => bool)) public hasRole;
|
|
|
|
event RoleSet(bytes32 indexed role, address indexed account, bool enabled);
|
|
|
|
function setRole(bytes32 role, address account, bool enabled) external onlyOwner {
|
|
hasRole[role][account] = enabled;
|
|
emit RoleSet(role, account, enabled);
|
|
}
|
|
}
|
|
|
|
contract PolicyRegistry is Owned {
|
|
struct Policy {
|
|
bool active;
|
|
bool lockupActive;
|
|
uint256 maxAutoApproveQuantity;
|
|
}
|
|
|
|
mapping(bytes32 => Policy) public policies;
|
|
|
|
event PolicySet(
|
|
bytes32 indexed policyId,
|
|
bool active,
|
|
bool lockupActive,
|
|
uint256 maxAutoApproveQuantity
|
|
);
|
|
|
|
function setPolicy(
|
|
bytes32 policyId,
|
|
bool active,
|
|
bool lockupActive,
|
|
uint256 maxAutoApproveQuantity
|
|
) external onlyOwner {
|
|
policies[policyId] = Policy({
|
|
active: active,
|
|
lockupActive: lockupActive,
|
|
maxAutoApproveQuantity: maxAutoApproveQuantity
|
|
});
|
|
|
|
emit PolicySet(policyId, active, lockupActive, maxAutoApproveQuantity);
|
|
}
|
|
|
|
function getPolicy(bytes32 policyId) external view returns (Policy memory) {
|
|
return policies[policyId];
|
|
}
|
|
}
|
|
|
|
contract TransferControlModule is Owned {
|
|
enum Decision {
|
|
Approved,
|
|
Rejected,
|
|
PendingReview
|
|
}
|
|
|
|
struct ControlRequest {
|
|
bytes32 instructionId;
|
|
bytes32 messageDefinitionId;
|
|
bytes32 lifecycleEvent;
|
|
bytes32 assetId;
|
|
bytes32 assetType;
|
|
address assetContract;
|
|
bytes32 networkId;
|
|
address fromWallet;
|
|
address toWallet;
|
|
uint256 quantity;
|
|
bytes32 policyId;
|
|
}
|
|
|
|
struct ControlDecision {
|
|
Decision decision;
|
|
bytes32 reasonCode;
|
|
bytes32 policyId;
|
|
bytes32 evidenceRef;
|
|
}
|
|
|
|
bytes32 public constant SECONDARY_TRANSFER = keccak256("SECONDARY_TRANSFER");
|
|
bytes32 public constant APPROVED = keccak256("APPROVED");
|
|
bytes32 public constant FROM_NOT_VERIFIED = keccak256("FROM_NOT_VERIFIED");
|
|
bytes32 public constant TO_NOT_VERIFIED = keccak256("TO_NOT_VERIFIED");
|
|
bytes32 public constant PARTY_FROZEN = keccak256("PARTY_FROZEN");
|
|
bytes32 public constant ASSET_NOT_ALLOWED = keccak256("ASSET_NOT_ALLOWED");
|
|
bytes32 public constant LOCKUP_ACTIVE = keccak256("LOCKUP_ACTIVE");
|
|
bytes32 public constant POLICY_BLOCKED = keccak256("POLICY_BLOCKED");
|
|
bytes32 public constant REVIEW_REQUIRED = keccak256("REVIEW_REQUIRED");
|
|
bytes32 public constant UNSUPPORTED_LIFECYCLE_EVENT = keccak256("UNSUPPORTED_LIFECYCLE_EVENT");
|
|
|
|
IdentityRegistry public identityRegistry;
|
|
PolicyRegistry public policyRegistry;
|
|
|
|
constructor(address identityRegistry_, address policyRegistry_) {
|
|
identityRegistry = IdentityRegistry(identityRegistry_);
|
|
policyRegistry = PolicyRegistry(policyRegistry_);
|
|
}
|
|
|
|
function evaluate(ControlRequest calldata request)
|
|
external
|
|
view
|
|
returns (ControlDecision memory)
|
|
{
|
|
if (request.lifecycleEvent != SECONDARY_TRANSFER) {
|
|
return _decision(Decision.Rejected, UNSUPPORTED_LIFECYCLE_EVENT, request.policyId);
|
|
}
|
|
|
|
IdentityRegistry.Party memory fromParty = identityRegistry.getParty(request.fromWallet);
|
|
IdentityRegistry.Party memory toParty = identityRegistry.getParty(request.toWallet);
|
|
PolicyRegistry.Policy memory policy = policyRegistry.getPolicy(request.policyId);
|
|
|
|
if (!fromParty.verified) {
|
|
return _decision(Decision.Rejected, FROM_NOT_VERIFIED, request.policyId);
|
|
}
|
|
|
|
if (!toParty.verified) {
|
|
return _decision(Decision.Rejected, TO_NOT_VERIFIED, request.policyId);
|
|
}
|
|
|
|
if (fromParty.frozen || toParty.frozen) {
|
|
return _decision(Decision.Rejected, PARTY_FROZEN, request.policyId);
|
|
}
|
|
|
|
if (!toParty.eligible) {
|
|
return _decision(Decision.Rejected, ASSET_NOT_ALLOWED, request.policyId);
|
|
}
|
|
|
|
if (!policy.active) {
|
|
return _decision(Decision.Rejected, POLICY_BLOCKED, request.policyId);
|
|
}
|
|
|
|
if (policy.lockupActive) {
|
|
return _decision(Decision.Rejected, LOCKUP_ACTIVE, request.policyId);
|
|
}
|
|
|
|
if (request.quantity > policy.maxAutoApproveQuantity) {
|
|
return _decision(Decision.PendingReview, REVIEW_REQUIRED, request.policyId);
|
|
}
|
|
|
|
return _decision(Decision.Approved, APPROVED, request.policyId);
|
|
}
|
|
|
|
function _decision(Decision decision, bytes32 reasonCode, bytes32 policyId)
|
|
internal
|
|
pure
|
|
returns (ControlDecision memory)
|
|
{
|
|
return ControlDecision({
|
|
decision: decision,
|
|
reasonCode: reasonCode,
|
|
policyId: policyId,
|
|
evidenceRef: keccak256("SYNTHETIC_EVIDENCE")
|
|
});
|
|
}
|
|
}
|
|
|
|
contract TokenizedBond is Owned {
|
|
string public name;
|
|
string public symbol;
|
|
uint8 public decimals = 0;
|
|
uint256 public totalSupply;
|
|
bytes32 public assetId;
|
|
bytes32 public assetType;
|
|
bytes32 public messageDefinitionId;
|
|
bytes32 public networkId;
|
|
bytes32 public policyId;
|
|
|
|
bytes32 public constant TRANSFER_AGENT_ROLE = keccak256("TRANSFER_AGENT_ROLE");
|
|
bytes32 public constant SECONDARY_TRANSFER = keccak256("SECONDARY_TRANSFER");
|
|
bytes32 public constant INSUFFICIENT_POSITION = keccak256("INSUFFICIENT_POSITION");
|
|
|
|
TransferControlModule public controlModule;
|
|
RoleRegistry public roleRegistry;
|
|
|
|
mapping(address => uint256) public balanceOf;
|
|
|
|
event Transfer(address indexed from, address indexed to, uint256 value);
|
|
event TransferControlDecision(
|
|
bytes32 indexed instructionId,
|
|
address indexed fromWallet,
|
|
address indexed toWallet,
|
|
uint8 decision,
|
|
bytes32 reasonCode,
|
|
bytes32 policyId,
|
|
bytes32 messageDefinitionId,
|
|
bytes32 assetType,
|
|
bytes32 networkId
|
|
);
|
|
|
|
modifier onlyTransferAgent() {
|
|
require(
|
|
msg.sender == owner || roleRegistry.hasRole(TRANSFER_AGENT_ROLE, msg.sender),
|
|
"ONLY_TRANSFER_AGENT"
|
|
);
|
|
_;
|
|
}
|
|
|
|
constructor(
|
|
string memory name_,
|
|
string memory symbol_,
|
|
bytes32 assetId_,
|
|
bytes32 assetType_,
|
|
bytes32 messageDefinitionId_,
|
|
bytes32 networkId_,
|
|
bytes32 policyId_,
|
|
address controlModule_,
|
|
address roleRegistry_,
|
|
address initialHolder,
|
|
uint256 initialSupply
|
|
) {
|
|
name = name_;
|
|
symbol = symbol_;
|
|
assetId = assetId_;
|
|
assetType = assetType_;
|
|
messageDefinitionId = messageDefinitionId_;
|
|
networkId = networkId_;
|
|
policyId = policyId_;
|
|
controlModule = TransferControlModule(controlModule_);
|
|
roleRegistry = RoleRegistry(roleRegistry_);
|
|
balanceOf[initialHolder] = initialSupply;
|
|
totalSupply = initialSupply;
|
|
emit Transfer(address(0), initialHolder, initialSupply);
|
|
}
|
|
|
|
function controlledTransfer(
|
|
bytes32 instructionId,
|
|
address fromWallet,
|
|
address toWallet,
|
|
uint256 quantity
|
|
) external onlyTransferAgent returns (uint8, bytes32) {
|
|
if (balanceOf[fromWallet] < quantity) {
|
|
emit TransferControlDecision(
|
|
instructionId,
|
|
fromWallet,
|
|
toWallet,
|
|
uint8(TransferControlModule.Decision.Rejected),
|
|
INSUFFICIENT_POSITION,
|
|
policyId,
|
|
messageDefinitionId,
|
|
assetType,
|
|
networkId
|
|
);
|
|
return (uint8(TransferControlModule.Decision.Rejected), INSUFFICIENT_POSITION);
|
|
}
|
|
|
|
TransferControlModule.ControlRequest memory request =
|
|
TransferControlModule.ControlRequest({
|
|
instructionId: instructionId,
|
|
messageDefinitionId: messageDefinitionId,
|
|
lifecycleEvent: SECONDARY_TRANSFER,
|
|
assetId: assetId,
|
|
assetType: assetType,
|
|
assetContract: address(this),
|
|
networkId: networkId,
|
|
fromWallet: fromWallet,
|
|
toWallet: toWallet,
|
|
quantity: quantity,
|
|
policyId: policyId
|
|
});
|
|
|
|
TransferControlModule.ControlDecision memory decision = controlModule.evaluate(request);
|
|
|
|
if (decision.decision == TransferControlModule.Decision.Approved) {
|
|
balanceOf[fromWallet] -= quantity;
|
|
balanceOf[toWallet] += quantity;
|
|
emit Transfer(fromWallet, toWallet, quantity);
|
|
}
|
|
|
|
emit TransferControlDecision(
|
|
instructionId,
|
|
fromWallet,
|
|
toWallet,
|
|
uint8(decision.decision),
|
|
decision.reasonCode,
|
|
decision.policyId,
|
|
messageDefinitionId,
|
|
assetType,
|
|
networkId
|
|
);
|
|
|
|
return (uint8(decision.decision), decision.reasonCode);
|
|
}
|
|
}
|